From b973cf5e61405b7c1cc42a58a4e1e68f040a60c2 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Thu, 25 Sep 2025 12:17:59 +0100 Subject: [PATCH 01/16] Use the latest version of the Mongo driver. This means targetting .NET 4.7.2 for vector data implementations that depend on it. Supressed warnings about disposal since it doesn't do anything yet. # Conflicts: # dotnet/Directory.Packages.props --- dotnet/Directory.Packages.props | 2 +- .../Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs | 2 ++ .../Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs | 2 ++ dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs | 2 +- dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj | 2 +- dotnet/src/VectorData/MongoDB/MongoDB.csproj | 2 +- .../Support/CosmosMongoTestStore.cs | 2 ++ 7 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index b72bb4325196..87b04a10a44e 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -161,7 +161,7 @@ - + diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs index 2aa0511e9289..ec3bb6613d21 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs @@ -40,7 +40,9 @@ public CosmosMongoVectorStoreFixture() .Build(); var connectionString = GetConnectionString(configuration); +#pragma warning disable CA2000 var client = new MongoClient(connectionString); +#pragma warning restore CA2000 this.MongoDatabase = client.GetDatabase("test"); diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs index 51ca1e3565da..304321bb77ac 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs @@ -37,11 +37,13 @@ public async Task InitializeAsync() cts.CancelAfter(TimeSpan.FromSeconds(60)); await this._container.StartAsync(cts.Token); +#pragma warning disable CA2000 var mongoClient = new MongoClient(new MongoClientSettings { Server = new MongoServerAddress(this._container.Hostname, this._container.GetMappedPublicPort(MongoDbBuilder.MongoDbPort)), DirectConnection = true, }); +#pragma warning restore CA2000 this.MongoDatabase = mongoClient.GetDatabase("test"); diff --git a/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs b/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs index 621025f1e39a..0232089e779d 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs @@ -99,7 +99,7 @@ private static HttpClientHandler CreateHandler() catch (PlatformNotSupportedException) { } // not supported on older frameworks return handler; } -#elif NET462 +#elif NET462 || NET472 private static HttpClientHandler CreateHandler() => new(); #endif diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj index 5a4b8ec8f965..d5aa55267351 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj @@ -4,7 +4,7 @@ Microsoft.SemanticKernel.Connectors.CosmosMongoDB $(AssemblyName) - net8.0;netstandard2.0;net462 + net8.0;net472 true preview diff --git a/dotnet/src/VectorData/MongoDB/MongoDB.csproj b/dotnet/src/VectorData/MongoDB/MongoDB.csproj index 30009285928a..0d1525d98ad0 100644 --- a/dotnet/src/VectorData/MongoDB/MongoDB.csproj +++ b/dotnet/src/VectorData/MongoDB/MongoDB.csproj @@ -4,7 +4,7 @@ Microsoft.SemanticKernel.Connectors.MongoDB $(AssemblyName) - net8.0;netstandard2.0;net462 + net8.0;net472 true preview diff --git a/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/Support/CosmosMongoTestStore.cs b/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/Support/CosmosMongoTestStore.cs index 2db6e9188e7d..280b42387451 100644 --- a/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/Support/CosmosMongoTestStore.cs +++ b/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/Support/CosmosMongoTestStore.cs @@ -6,7 +6,9 @@ namespace CosmosMongoDB.ConformanceTests.Support; +#pragma warning disable CA1001 public sealed class CosmosMongoTestStore : TestStore +#pragma warning restore CA1001 { public static CosmosMongoTestStore Instance { get; } = new(); From 425efa4924a12954c1e0adab625ebcbee54132ac Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 17 Nov 2025 09:08:58 +0000 Subject: [PATCH 02/16] Ensure Guid binary standard serialization. --- .../Memory/MongoDB/MongoDynamicMapper.cs | 2 +- .../connectors/Memory/MongoDB/MongoMapper.cs | 15 +++++++++- .../src/VectorData/MongoDB/MongoCollection.cs | 28 +++++++++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs index 71931f75ed1d..19e5502fc143 100644 --- a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs +++ b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs @@ -30,7 +30,7 @@ public BsonDocument MapFromDataToStorageModel(Dictionary dataMo : keyValue switch { string s => s, - Guid g => BsonValue.Create(g), + Guid g => new BsonBinaryData(g, GuidRepresentation.Standard), ObjectId o => o, long i => i, int i => i, diff --git a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoMapper.cs b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoMapper.cs index 85a5d0462bb0..5f42b66a3849 100644 --- a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoMapper.cs +++ b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoMapper.cs @@ -11,6 +11,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Serializers; namespace Microsoft.SemanticKernel.Connectors.MongoDB; @@ -40,7 +41,8 @@ public MongoMapper(CollectionModel model) var conventionPack = new ConventionPack { - new IgnoreExtraElementsConvention(ignoreExtraElements: true) + new IgnoreExtraElementsConvention(ignoreExtraElements: true), + new GuidStandardRepresentationConvention() }; ConventionRegistry.Register( @@ -139,4 +141,15 @@ public TRecord MapFromStorageToDataModel(BsonDocument storageModel, bool include return BsonSerializer.Deserialize(storageModel); } + + private class GuidStandardRepresentationConvention : ConventionBase, IMemberMapConvention + { + public void Apply(BsonMemberMap memberMap) + { + if (memberMap.MemberType == typeof(Guid) && memberMap.MemberInfo.GetCustomAttribute() is null) + { + memberMap.SetSerializer(new GuidSerializer(GuidRepresentation.Standard)); + } + } + } } diff --git a/dotnet/src/VectorData/MongoDB/MongoCollection.cs b/dotnet/src/VectorData/MongoDB/MongoCollection.cs index 565667bda0e3..a3bd39f53cbb 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollection.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollection.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.VectorData; using Microsoft.Extensions.VectorData.ProviderServices; using MongoDB.Bson; +using MongoDB.Bson.Serialization; using MongoDB.Driver; using MEVD = Microsoft.Extensions.VectorData; @@ -75,6 +76,9 @@ public class MongoCollection : VectorStoreCollectionNumber of nearest neighbors to use during the vector search. private readonly int? _numCandidates; + /// to use for serializing key values. + private readonly BsonSerializationInfo _keySerializationInfo; + /// Types of keys permitted. private static readonly Type[] s_validKeyTypes = [typeof(string), typeof(Guid), typeof(ObjectId), typeof(int), typeof(long)]; @@ -135,6 +139,8 @@ internal MongoCollection(IMongoDatabase mongoDatabase, string name, Func @@ -676,10 +682,28 @@ private async IAsyncEnumerable> EnumerateAndMapSearc } private FilterDefinition GetFilterById(TKey id) - => Builders.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, id); + => Builders.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, this._keySerializationInfo.SerializeValue(id)); private FilterDefinition GetFilterByIds(IEnumerable ids) - => Builders.Filter.In(MongoConstants.MongoReservedKeyPropertyName, ids); + => Builders.Filter.In(MongoConstants.MongoReservedKeyPropertyName, this._keySerializationInfo.SerializeValues(ids)); + + private BsonSerializationInfo GetKeySerializationInfo() + { + var documentSerializer = BsonSerializer.LookupSerializer(); + Verify.NotNull(documentSerializer, $"BsonSerializer not found for type '{typeof(TRecord)}'"); + + if (documentSerializer is not IBsonDocumentSerializer bsonDocumentSerializer) + { + throw new InvalidOperationException($"BsonSerializer for type '{typeof(TRecord)}' does not implement IBsonDocumentSerializer"); + } + + if (!bsonDocumentSerializer.TryGetMemberSerializationInfo(this._model.KeyProperty.ModelName, out var keySerializationInfo)) + { + throw new InvalidOperationException($"BsonSerializer for type '{typeof(TRecord)}' does not recognize key property {this._model.KeyProperty.ModelName}"); + } + + return keySerializationInfo; + } private async Task InternalCollectionExistsAsync(CancellationToken cancellationToken) { From 0e3aa43a1ca9a5e6c2dd35fe6f4a37a5f2553bf6 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 17 Nov 2025 14:06:29 +0000 Subject: [PATCH 03/16] Introduce BsonValueFactory and handle lookup by Guid on dyanmic mapper. --- .../Memory/MongoDB/MongoDynamicMapper.cs | 6 ++- .../CosmosMongoCollectionSearchMapping.cs | 2 +- .../CosmosMongoDB/CosmosMongoDB.csproj | 3 ++ .../CosmosMongoFilterTranslator.cs | 6 +-- .../VectorData/MongoDB/BsonValueFactory.cs | 42 +++++++++++++++++++ .../src/VectorData/MongoDB/MongoCollection.cs | 14 ++++++- .../MongoDB/MongoCollectionSearchMapping.cs | 2 +- .../MongoDB/MongoFilterTranslator.cs | 6 +-- 8 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 dotnet/src/VectorData/MongoDB/BsonValueFactory.cs diff --git a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs index 19e5502fc143..2e4b6dc58139 100644 --- a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs +++ b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs @@ -36,14 +36,14 @@ public BsonDocument MapFromDataToStorageModel(Dictionary dataMo int i => i, null => throw new InvalidOperationException($"Key property '{model.KeyProperty.ModelName}' is null."), - _ => throw new InvalidCastException($"Key property '{model.KeyProperty.ModelName}' must be a string.") + _ => throw new InvalidCastException($"Key property '{model.KeyProperty.ModelName}' must be a string, Guid, ObjectId, long or int.") }; foreach (var property in model.DataProperties) { if (dataModel.TryGetValue(property.ModelName, out var dataValue)) { - document[property.StorageName] = BsonValue.Create(dataValue); + document[property.StorageName] = BsonValueFactory.Create(dataValue); } } @@ -167,6 +167,8 @@ Embedding e Type t when t == typeof(decimal?) => value.AsNullableDecimal, Type t when t == typeof(DateTime) => value.ToUniversalTime(), Type t when t == typeof(DateTime?) => value.ToNullableUniversalTime(), + Type t when t == typeof(Guid) => value.AsGuid, + Type t when t == typeof(Guid?) => value.AsNullableGuid, _ => (object?)null }; diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionSearchMapping.cs b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionSearchMapping.cs index ea40edd73618..8cd011218e09 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionSearchMapping.cs +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionSearchMapping.cs @@ -50,7 +50,7 @@ internal static class CosmosMongoCollectionSearchMapping if (filterClause is EqualToFilterClause equalToFilterClause) { propertyName = equalToFilterClause.FieldName; - propertyValue = BsonValue.Create(equalToFilterClause.Value); + propertyValue = BsonValueFactory.Create(equalToFilterClause.Value); filterOperator = EqualOperator; } else diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj index d5aa55267351..d3e71192077d 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj @@ -15,6 +15,9 @@ + + BsonValueFactory.cs + diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs index 6d8401d4d602..841fd00004bf 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs @@ -81,7 +81,7 @@ private BsonDocument GenerateEqualityComparison(PropertyModel property, object? // Short form of equality (instead of $eq) if (nodeType is ExpressionType.Equal) { - return new BsonDocument { [property.StorageName] = BsonValue.Create(value) }; + return new BsonDocument { [property.StorageName] = BsonValueFactory.Create(value) }; } var filterOperator = nodeType switch @@ -95,7 +95,7 @@ private BsonDocument GenerateEqualityComparison(PropertyModel property, object? _ => throw new UnreachableException() }; - return new BsonDocument { [property.StorageName] = new BsonDocument { [filterOperator] = BsonValue.Create(value) } }; + return new BsonDocument { [property.StorageName] = new BsonDocument { [filterOperator] = BsonValueFactory.Create(value) } }; } private BsonDocument TranslateAndOr(BinaryExpression andOr) @@ -257,7 +257,7 @@ BsonDocument ProcessInlineEnumerable(IEnumerable elements, Expression item) { [property.StorageName] = new BsonDocument { - ["$in"] = new BsonArray(from object? element in elements select BsonValue.Create(element)) + ["$in"] = new BsonArray(from object? element in elements select BsonValueFactory.Create(element)) } }; } diff --git a/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs b/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs new file mode 100644 index 000000000000..f1bc31778a9c --- /dev/null +++ b/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using MongoDB.Bson; + +namespace Microsoft.SemanticKernel.Connectors.MongoDB; + +/// +/// A class that constructs the correct BsonValue for a given CLR type. +/// +internal static class BsonValueFactory +{ + /// + /// Create a BsonValue for the given CLR type. + /// + /// The CLR object to create a BSON value for. + /// The appropriate for that CLR type. + public static BsonValue Create(object? value) + { + if (value is null) + { + return BsonNull.Value; + } + + if (value.GetType().IsArray) + { + if (value is Guid[] guids) + { + return new BsonArray(Array.ConvertAll(guids, x => new BsonBinaryData(x, GuidRepresentation.Standard))); + } + + return new BsonArray(value as Array); + } + + if (value is Guid guid) + { + return new BsonBinaryData(guid, GuidRepresentation.Standard); + } + + return BsonValue.Create(value); + } +} diff --git a/dotnet/src/VectorData/MongoDB/MongoCollection.cs b/dotnet/src/VectorData/MongoDB/MongoCollection.cs index a3bd39f53cbb..7b89d769d643 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollection.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollection.cs @@ -682,10 +682,20 @@ private async IAsyncEnumerable> EnumerateAndMapSearc } private FilterDefinition GetFilterById(TKey id) - => Builders.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, this._keySerializationInfo.SerializeValue(id)); + { + var bsonValue = id.GetType() == typeof(TKey) + ? this._keySerializationInfo.SerializeValue(id) + : BsonValueFactory.Create(id); + return Builders.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, bsonValue); + } private FilterDefinition GetFilterByIds(IEnumerable ids) - => Builders.Filter.In(MongoConstants.MongoReservedKeyPropertyName, this._keySerializationInfo.SerializeValues(ids)); + { + var bsonValues = ids.GetType().GetElementType() == typeof(TKey) + ? (BsonArray)this._keySerializationInfo.SerializeValues(ids) + : (BsonArray)BsonValueFactory.Create(ids); + return Builders.Filter.In(MongoConstants.MongoReservedKeyPropertyName, bsonValues); + } private BsonSerializationInfo GetKeySerializationInfo() { diff --git a/dotnet/src/VectorData/MongoDB/MongoCollectionSearchMapping.cs b/dotnet/src/VectorData/MongoDB/MongoCollectionSearchMapping.cs index 12ab41934177..7d50f1ee9688 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollectionSearchMapping.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollectionSearchMapping.cs @@ -46,7 +46,7 @@ internal static class MongoCollectionSearchMapping if (filterClause is EqualToFilterClause equalToFilterClause) { propertyName = equalToFilterClause.FieldName; - propertyValue = BsonValue.Create(equalToFilterClause.Value); + propertyValue = BsonValueFactory.Create(equalToFilterClause.Value); filterOperator = EqualOperator; } else diff --git a/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs b/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs index c6a7788cc804..0524b21b2d73 100644 --- a/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs +++ b/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs @@ -87,7 +87,7 @@ private BsonDocument GenerateEqualityComparison(PropertyModel property, object? // Short form of equality (instead of $eq) if (nodeType is ExpressionType.Equal) { - return new BsonDocument { [property.StorageName] = BsonValue.Create(value) }; + return new BsonDocument { [property.StorageName] = BsonValueFactory.Create(value) }; } var filterOperator = nodeType switch @@ -101,7 +101,7 @@ private BsonDocument GenerateEqualityComparison(PropertyModel property, object? _ => throw new UnreachableException() }; - return new BsonDocument { [property.StorageName] = new BsonDocument { [filterOperator] = BsonValue.Create(value) } }; + return new BsonDocument { [property.StorageName] = new BsonDocument { [filterOperator] = BsonValueFactory.Create(value) } }; } private BsonDocument TranslateAndOr(BinaryExpression andOr) @@ -261,7 +261,7 @@ BsonDocument ProcessInlineEnumerable(IEnumerable elements, Expression item) { [property.StorageName] = new BsonDocument { - ["$in"] = new BsonArray(from object? element in elements select BsonValue.Create(element)) + ["$in"] = new BsonArray(from object? element in elements select BsonValueFactory.Create(element)) } }; } From e6d44462af1653a0afd122b4ef60aa47df7d0834 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 18 Nov 2025 13:06:35 +0000 Subject: [PATCH 04/16] Clarify serializer vs BsonFactory in MongoCollection. --- .../src/VectorData/MongoDB/MongoCollection.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dotnet/src/VectorData/MongoDB/MongoCollection.cs b/dotnet/src/VectorData/MongoDB/MongoCollection.cs index 7b89d769d643..bfbdaa58e228 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollection.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollection.cs @@ -77,7 +77,7 @@ public class MongoCollection : VectorStoreCollection to use for serializing key values. - private readonly BsonSerializationInfo _keySerializationInfo; + private readonly BsonSerializationInfo? _keySerializationInfo; /// Types of keys permitted. private static readonly Type[] s_validKeyTypes = [typeof(string), typeof(Guid), typeof(ObjectId), typeof(int), typeof(long)]; @@ -140,7 +140,10 @@ internal MongoCollection(IMongoDatabase mongoDatabase, string name, Func @@ -683,17 +686,15 @@ private async IAsyncEnumerable> EnumerateAndMapSearc private FilterDefinition GetFilterById(TKey id) { - var bsonValue = id.GetType() == typeof(TKey) - ? this._keySerializationInfo.SerializeValue(id) - : BsonValueFactory.Create(id); + // Use cached key serialization info but fall back to BsonValueFactory for dynamic mapper. + var bsonValue = this._keySerializationInfo?.SerializeValue(id) ?? BsonValueFactory.Create(id); return Builders.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, bsonValue); } private FilterDefinition GetFilterByIds(IEnumerable ids) { - var bsonValues = ids.GetType().GetElementType() == typeof(TKey) - ? (BsonArray)this._keySerializationInfo.SerializeValues(ids) - : (BsonArray)BsonValueFactory.Create(ids); + // Use cached key serialization info but fall back to BsonValueFactory for dynamic mapper. + var bsonValues = this._keySerializationInfo.SerializeValues(ids) ?? (BsonArray)BsonValueFactory.Create(ids); return Builders.Filter.In(MongoConstants.MongoReservedKeyPropertyName, bsonValues); } From 729e565a45acc08d306a6d36d527af37b2f57556 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 20 Nov 2025 08:51:52 +0000 Subject: [PATCH 05/16] Bump MongoDB.Driver to 3.5.1 --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 87b04a10a44e..5454a2ab7f5c 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -161,7 +161,7 @@ - + From 5ae65d81ef2e0d044aac22545fe051779bb4c575 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 25 Nov 2025 11:57:57 +0000 Subject: [PATCH 06/16] Address PR feedback by adding NETSTANDARD2_1 and coding style. --- .../CosmosMongoDB/CosmosMongoDB.csproj | 2 +- .../VectorData/MongoDB/BsonValueFactory.cs | 29 +++++-------------- .../src/VectorData/MongoDB/MongoCollection.cs | 2 +- dotnet/src/VectorData/MongoDB/MongoDB.csproj | 2 +- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj index d3e71192077d..48280b527a37 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj @@ -4,7 +4,7 @@ Microsoft.SemanticKernel.Connectors.CosmosMongoDB $(AssemblyName) - net8.0;net472 + net8.0;netstandard2.1;net472 true preview diff --git a/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs b/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs index f1bc31778a9c..ee2a79ba8971 100644 --- a/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs +++ b/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs @@ -16,27 +16,12 @@ internal static class BsonValueFactory /// The CLR object to create a BSON value for. /// The appropriate for that CLR type. public static BsonValue Create(object? value) - { - if (value is null) + => value switch { - return BsonNull.Value; - } - - if (value.GetType().IsArray) - { - if (value is Guid[] guids) - { - return new BsonArray(Array.ConvertAll(guids, x => new BsonBinaryData(x, GuidRepresentation.Standard))); - } - - return new BsonArray(value as Array); - } - - if (value is Guid guid) - { - return new BsonBinaryData(guid, GuidRepresentation.Standard); - } - - return BsonValue.Create(value); - } + null => BsonNull.Value, + Guid guid => new BsonBinaryData(guid, GuidRepresentation.Standard), + Guid[] guids => new BsonArray(Array.ConvertAll(guids, x => new BsonBinaryData(x, GuidRepresentation.Standard))), + Array array => new BsonArray(array), + _ => BsonValue.Create(value) + }; } diff --git a/dotnet/src/VectorData/MongoDB/MongoCollection.cs b/dotnet/src/VectorData/MongoDB/MongoCollection.cs index bfbdaa58e228..ca30f1d7f21c 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollection.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollection.cs @@ -694,7 +694,7 @@ private FilterDefinition GetFilterById(TKey id) private FilterDefinition GetFilterByIds(IEnumerable ids) { // Use cached key serialization info but fall back to BsonValueFactory for dynamic mapper. - var bsonValues = this._keySerializationInfo.SerializeValues(ids) ?? (BsonArray)BsonValueFactory.Create(ids); + var bsonValues = this._keySerializationInfo?.SerializeValues(ids) ?? (BsonArray)BsonValueFactory.Create(ids); return Builders.Filter.In(MongoConstants.MongoReservedKeyPropertyName, bsonValues); } diff --git a/dotnet/src/VectorData/MongoDB/MongoDB.csproj b/dotnet/src/VectorData/MongoDB/MongoDB.csproj index 0d1525d98ad0..e33f8b42e02b 100644 --- a/dotnet/src/VectorData/MongoDB/MongoDB.csproj +++ b/dotnet/src/VectorData/MongoDB/MongoDB.csproj @@ -4,7 +4,7 @@ Microsoft.SemanticKernel.Connectors.MongoDB $(AssemblyName) - net8.0;net472 + net8.0;netstandard2.1;net472 true preview From 48fa5036312fba44b7b3b5c36b6a58e1c0d79619 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 25 Nov 2025 15:30:17 +0000 Subject: [PATCH 07/16] PR feedback. --- .../InternalUtilities/src/Diagnostics/NullableAttributes.cs | 3 ++- dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs | 4 ++-- dotnet/src/InternalUtilities/src/System/IndexRange.cs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs b/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs index 91d716132ced..7996d2b292bd 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs @@ -7,9 +7,10 @@ // This was copied from https://github.com/dotnet/runtime/blob/39b9607807f29e48cae4652cd74735182b31182e/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs // and updated to have the scope of the attributes be internal. -#if !NETCOREAPP namespace System.Diagnostics.CodeAnalysis; +#if !NETCOREAPP && !NETSTANDARD2_1 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] internal sealed class AllowNullAttribute : Attribute diff --git a/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs b/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs index 0232089e779d..3a6b5b9fe111 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs @@ -88,7 +88,7 @@ private static SocketsHttpHandler CreateHandler() }, }; } -#elif NETSTANDARD2_0 +#elif NETSTANDARD2_0_OR_GREATER private static HttpClientHandler CreateHandler() { var handler = new HttpClientHandler(); @@ -99,7 +99,7 @@ private static HttpClientHandler CreateHandler() catch (PlatformNotSupportedException) { } // not supported on older frameworks return handler; } -#elif NET462 || NET472 +#elif NETFRAMEWORK private static HttpClientHandler CreateHandler() => new(); #endif diff --git a/dotnet/src/InternalUtilities/src/System/IndexRange.cs b/dotnet/src/InternalUtilities/src/System/IndexRange.cs index d27fa32aa513..c3ecbeb24781 100644 --- a/dotnet/src/InternalUtilities/src/System/IndexRange.cs +++ b/dotnet/src/InternalUtilities/src/System/IndexRange.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -#if !NET8_0_OR_GREATER +#if !NET8_0_OR_GREATER && !NETSTANDARD2_1 // Polyfill for using Index and Range with .NET Standard 2.0 (see https://www.meziantou.net/how-to-use-csharp-8-indices-and-ranges-in-dotnet-standard-2-0-and-dotn.htm) From f42582031b20f24d04f2cc08efe6a45baaf32480 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:10:28 +0000 Subject: [PATCH 08/16] Update dotnet/src/VectorData/MongoDB/MongoCollection.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --- dotnet/src/VectorData/MongoDB/MongoCollection.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dotnet/src/VectorData/MongoDB/MongoCollection.cs b/dotnet/src/VectorData/MongoDB/MongoCollection.cs index ca30f1d7f21c..9e90e63b6530 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollection.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollection.cs @@ -701,7 +701,10 @@ private FilterDefinition GetFilterByIds(IEnumerable ids) private BsonSerializationInfo GetKeySerializationInfo() { var documentSerializer = BsonSerializer.LookupSerializer(); - Verify.NotNull(documentSerializer, $"BsonSerializer not found for type '{typeof(TRecord)}'"); +if (documentSerializer is null) +{ + throw new InvalidOperationException($"BsonSerializer not found for type '{typeof(TRecord)}'"); +} if (documentSerializer is not IBsonDocumentSerializer bsonDocumentSerializer) { From 8daf05afe1dbea54ab31e611cbcb2c17dee7e93a Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:22:21 +0000 Subject: [PATCH 09/16] Fix formatting of null check for documentSerializer --- dotnet/src/VectorData/MongoDB/MongoCollection.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/VectorData/MongoDB/MongoCollection.cs b/dotnet/src/VectorData/MongoDB/MongoCollection.cs index 9e90e63b6530..c52d3c25bc6d 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollection.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollection.cs @@ -701,10 +701,10 @@ private FilterDefinition GetFilterByIds(IEnumerable ids) private BsonSerializationInfo GetKeySerializationInfo() { var documentSerializer = BsonSerializer.LookupSerializer(); -if (documentSerializer is null) -{ - throw new InvalidOperationException($"BsonSerializer not found for type '{typeof(TRecord)}'"); -} + if (documentSerializer is null) + { + throw new InvalidOperationException($"BsonSerializer not found for type '{typeof(TRecord)}'"); + } if (documentSerializer is not IBsonDocumentSerializer bsonDocumentSerializer) { From 9eee0b5b987b9e6f16a89ceaa29641308786d0a2 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 1 Dec 2025 20:57:48 +0000 Subject: [PATCH 10/16] Add missing merge line. --- dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj index 52b98d55b40e..921efdecdd4d 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj @@ -15,6 +15,9 @@ + + BsonValueFactory.cs + From 31172d6c39a7b358c725968d1710bd23103a68f3 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 2 Dec 2025 17:29:44 +0000 Subject: [PATCH 11/16] Bump MongoDB C# Driver to 3.5.2 --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index a3c4d8ccfd8d..f16128c50b63 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -161,7 +161,7 @@ - + From 0915d94a9f5d3dcb46df28fea00d7eacdb50863b Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 26 Sep 2025 13:51:49 +0100 Subject: [PATCH 12/16] Add alternative wait strategy for MongoDB container. --- .../Support/MongoTestStore.cs | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs b/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs index 3a7c3fef820d..2b901f6bb73d 100644 --- a/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs +++ b/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs @@ -19,8 +19,8 @@ internal sealed class MongoTestStore : TestStore private MongoDbContainer? _container; - public MongoClient? _client { get; private set; } - public IMongoDatabase? _database { get; private set; } + private MongoClient? _client; + private IMongoDatabase? _database; public MongoClient Client => this._client ?? throw new InvalidOperationException("Not initialized"); public IMongoDatabase Database => this._database ?? throw new InvalidOperationException("Not initialized"); @@ -46,8 +46,8 @@ protected override async Task StartAsync() private async Task StartMongoDbContainerAsync() { this._container = new MongoDbBuilder() - .WithImage("mongodb/mongodb-atlas-local:7.0.6") - .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new MongoDbWaitUntil())) + .WithImage("mongodb/mongodb-atlas-local:latest") + .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitForVectorIndexService())) .Build(); using CancellationTokenSource cts = new(); @@ -84,15 +84,56 @@ protected override async Task StopAsync() } } - private sealed class MongoDbWaitUntil : IWaitUntil + private sealed class WaitForVectorIndexService : IWaitUntil { - /// public async Task UntilAsync(IContainer container) { - var (stdout, _) = await container.GetLogsAsync(timestampsEnabled: false) - .ConfigureAwait(false); - - return stdout.Contains("\"msg\":\"Waiting for connections\""); + var connectionString = $"mongodb://{container.Hostname}:{container.GetMappedPublicPort(27017).ToString()}?directConnection=true"; + using var client = new MongoClient(connectionString); + var databaseName = Guid.NewGuid().ToString(); + var weGood = false; + + try + { + var database = client.GetDatabase(databaseName); + var collectionName = Guid.NewGuid().ToString(); + await database.CreateCollectionAsync(collectionName); + + var model = new CreateSearchIndexModel( + Guid.NewGuid().ToString(), + SearchIndexType.VectorSearch, + BsonDocument.Parse( + """ + { + "fields": [ + { + "type": "vector", + "path": "Dummy", + "numDimensions": 8, + "similarity": "cosine" + } + ] + } + """)); + + await database.GetCollection(collectionName).SearchIndexes.CreateOneAsync(model); + weGood = true; + } + catch + { + // Intentionally ignored. + } + + try + { + await client.DropDatabaseAsync(databaseName); + } + catch + { + // Intentionally ignored. + } + + return weGood; } } } From 161b49b6a9159242116f2624b2f6710ef835f22d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 3 Dec 2025 14:26:30 +0000 Subject: [PATCH 13/16] Add support for Guid in other Enumerables like ReadOnly. --- dotnet/src/VectorData/MongoDB/BsonValueFactory.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs b/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs index ee2a79ba8971..07e0508db462 100644 --- a/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs +++ b/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; +using System.Linq; using MongoDB.Bson; namespace Microsoft.SemanticKernel.Connectors.MongoDB; @@ -20,8 +22,9 @@ public static BsonValue Create(object? value) { null => BsonNull.Value, Guid guid => new BsonBinaryData(guid, GuidRepresentation.Standard), - Guid[] guids => new BsonArray(Array.ConvertAll(guids, x => new BsonBinaryData(x, GuidRepresentation.Standard))), + Object[] array => new BsonArray(Array.ConvertAll(array, Create)), Array array => new BsonArray(array), + IEnumerable enumerable => new BsonArray(enumerable.Select(Create)), _ => BsonValue.Create(value) }; } From 15fa29e046901db57f3e2e526e04f5cf8cf94fb8 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 3 Dec 2025 22:33:37 +0100 Subject: [PATCH 14/16] Fixes to Cosmos Mongo --- .../CosmosMongoVectorStoreFixture.cs | 2 +- .../MongoDB/MongoDBVectorStoreFixture.cs | 2 +- .../Memory}/MongoDB/BsonValueFactory.cs | 0 .../CosmosMongoDB/CosmosMongoCollection.cs | 67 ++++++++++++++----- .../CosmosMongoCollectionCreateMapping.cs | 4 +- .../CosmosMongoDB/CosmosMongoDB.csproj | 3 - .../CosmosMongoDistanceFunctionTests.cs | 2 +- .../CosmosMongoIndexKindTests.cs | 14 ++++ 8 files changed, 71 insertions(+), 23 deletions(-) rename dotnet/src/{VectorData => InternalUtilities/connectors/Memory}/MongoDB/BsonValueFactory.cs (100%) diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs index ec3bb6613d21..f4427b6d5bef 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/CosmosMongoDB/CosmosMongoVectorStoreFixture.cs @@ -40,7 +40,7 @@ public CosmosMongoVectorStoreFixture() .Build(); var connectionString = GetConnectionString(configuration); -#pragma warning disable CA2000 +#pragma warning disable CA2000 // Dispose objects before losing scope var client = new MongoClient(connectionString); #pragma warning restore CA2000 diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs index 304321bb77ac..5a17920cff6f 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBVectorStoreFixture.cs @@ -37,7 +37,7 @@ public async Task InitializeAsync() cts.CancelAfter(TimeSpan.FromSeconds(60)); await this._container.StartAsync(cts.Token); -#pragma warning disable CA2000 +#pragma warning disable CA2000 // Dispose objects before losing scope var mongoClient = new MongoClient(new MongoClientSettings { Server = new MongoServerAddress(this._container.Hostname, this._container.GetMappedPublicPort(MongoDbBuilder.MongoDbPort)), diff --git a/dotnet/src/VectorData/MongoDB/BsonValueFactory.cs b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/BsonValueFactory.cs similarity index 100% rename from dotnet/src/VectorData/MongoDB/BsonValueFactory.cs rename to dotnet/src/InternalUtilities/connectors/Memory/MongoDB/BsonValueFactory.cs diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollection.cs b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollection.cs index 16dba308f78f..6131f439f624 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollection.cs +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollection.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.VectorData.ProviderServices; using Microsoft.SemanticKernel.Connectors.MongoDB; using MongoDB.Bson; +using MongoDB.Bson.Serialization; using MongoDB.Driver; using MEVD = Microsoft.Extensions.VectorData; @@ -67,6 +68,9 @@ public class CosmosMongoCollection : VectorStoreCollectionThe size of the dynamic candidate list for search. private readonly int _efSearch; + /// to use for serializing key values. + private readonly BsonSerializationInfo? _keySerializationInfo; + private static readonly Type[] s_validKeyTypes = [typeof(string), typeof(Guid), typeof(ObjectId), typeof(int), typeof(long)]; /// @@ -123,6 +127,11 @@ internal CosmosMongoCollection(IMongoDatabase mongoDatabase, string name, Func @@ -142,9 +151,9 @@ await this.RunOperationAsync("CreateIndexes", /// public override async Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) { - var stringKey = this.GetStringKey(key); + Verify.NotNull(key); - await this.RunOperationAsync("DeleteOne", () => this._mongoCollection.DeleteOneAsync(this.GetFilterById(stringKey), cancellationToken)) + await this.RunOperationAsync("DeleteOne", () => this._mongoCollection.DeleteOneAsync(this.GetFilterById(key), cancellationToken)) .ConfigureAwait(false); } @@ -153,9 +162,7 @@ public override async Task DeleteAsync(IEnumerable keys, CancellationToken { Verify.NotNull(keys); - var stringKeys = keys is IEnumerable k ? k : keys.Cast(); - - await this.RunOperationAsync("DeleteMany", () => this._mongoCollection.DeleteManyAsync(this.GetFilterByIds(stringKeys), cancellationToken)) + await this.RunOperationAsync("DeleteMany", () => this._mongoCollection.DeleteManyAsync(this.GetFilterByIds(keys), cancellationToken)) .ConfigureAwait(false); } @@ -166,7 +173,7 @@ public override Task EnsureCollectionDeletedAsync(CancellationToken cancellation /// public override async Task GetAsync(TKey key, RecordRetrievalOptions? options = null, CancellationToken cancellationToken = default) { - var stringKey = this.GetStringKey(key); + Verify.NotNull(key); var includeVectors = options?.IncludeVectors ?? false; if (includeVectors && this._model.EmbeddingGenerationRequired) @@ -175,7 +182,7 @@ public override Task EnsureCollectionDeletedAsync(CancellationToken cancellation } using var cursor = await this - .FindAsync(this.GetFilterById(stringKey), top: 1, skip: null, includeVectors, sortDefinition: null, cancellationToken) + .FindAsync(this.GetFilterById(key), top: 1, skip: null, includeVectors, sortDefinition: null, cancellationToken) .ConfigureAwait(false); var record = await cursor.SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -202,10 +209,8 @@ public override async IAsyncEnumerable GetAsync( throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration); } - var stringKeys = keys is IEnumerable k ? k : keys.Cast(); - using var cursor = await this - .FindAsync(this.GetFilterByIds(stringKeys), top: null, skip: null, includeVectors, sortDefinition: null, cancellationToken) + .FindAsync(this.GetFilterByIds(keys), top: null, skip: null, includeVectors, sortDefinition: null, cancellationToken) .ConfigureAwait(false); while (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false)) @@ -252,7 +257,7 @@ private async Task UpsertCoreAsync(TRecord record, int recordIndex, IReadOnlyLis var replaceOptions = new ReplaceOptions { IsUpsert = true }; var storageModel = this._mapper.MapFromDataToStorageModel(record, recordIndex, generatedEmbeddings); - var key = storageModel[MongoConstants.MongoReservedKeyPropertyName].AsString; + var key = GetStorageKey(storageModel); await this.RunOperationAsync(OperationName, async () => await this._mongoCollection @@ -260,6 +265,9 @@ await this._mongoCollection .ConfigureAwait(false)).ConfigureAwait(false); } + private static TKey GetStorageKey(BsonDocument document) + => (TKey)BsonTypeMapper.MapToDotNetValue(document[MongoConstants.MongoReservedKeyPropertyName]); + private static async ValueTask<(IEnumerable records, IReadOnlyList?[]?)> ProcessEmbeddingsAsync( CollectionModel model, IEnumerable records, @@ -562,11 +570,40 @@ private async IAsyncEnumerable> EnumerateAndMapSearc } } - private FilterDefinition GetFilterById(string id) - => Builders.Filter.Eq(document => document[MongoConstants.MongoReservedKeyPropertyName], id); + private FilterDefinition GetFilterById(TKey id) + { + // Use cached key serialization info but fall back to BsonValueFactory for dynamic mapper. + var bsonValue = this._keySerializationInfo?.SerializeValue(id) ?? BsonValueFactory.Create(id); + return Builders.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, bsonValue); + } + + private FilterDefinition GetFilterByIds(IEnumerable ids) + { + // Use cached key serialization info but fall back to BsonValueFactory for dynamic mapper. + var bsonValues = this._keySerializationInfo?.SerializeValues(ids) ?? (BsonArray)BsonValueFactory.Create(ids); + return Builders.Filter.In(MongoConstants.MongoReservedKeyPropertyName, bsonValues); + } - private FilterDefinition GetFilterByIds(IEnumerable ids) - => Builders.Filter.In(document => document[MongoConstants.MongoReservedKeyPropertyName].AsString, ids); + private BsonSerializationInfo GetKeySerializationInfo() + { + var documentSerializer = BsonSerializer.LookupSerializer(); + if (documentSerializer is null) + { + throw new InvalidOperationException($"BsonSerializer not found for type '{typeof(TRecord)}'"); + } + + if (documentSerializer is not IBsonDocumentSerializer bsonDocumentSerializer) + { + throw new InvalidOperationException($"BsonSerializer for type '{typeof(TRecord)}' does not implement IBsonDocumentSerializer"); + } + + if (!bsonDocumentSerializer.TryGetMemberSerializationInfo(this._model.KeyProperty.ModelName, out var keySerializationInfo)) + { + throw new InvalidOperationException($"BsonSerializer for type '{typeof(TRecord)}' does not recognize key property {this._model.KeyProperty.ModelName}"); + } + + return keySerializationInfo; + } private async Task InternalCollectionExistsAsync(CancellationToken cancellationToken) { diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs index 7fa1a47ece87..08ab4c859e7e 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs @@ -112,7 +112,7 @@ private static string GetIndexKind(string? indexKind, string vectorPropertyName) { IndexKind.Hnsw => "vector-hnsw", IndexKind.IvfFlat => "vector-ivf", - _ => throw new InvalidOperationException($"Index kind '{indexKind}' on {nameof(VectorStoreVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB for MongoDB VectorStore.") + _ => throw new NotSupportedException($"Index kind '{indexKind}' on {nameof(VectorStoreVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB for MongoDB VectorStore.") }; /// @@ -124,6 +124,6 @@ private static string GetDistanceFunction(string? distanceFunction, string vecto DistanceFunction.CosineDistance => "COS", DistanceFunction.DotProductSimilarity => "IP", DistanceFunction.EuclideanDistance => "L2", - _ => throw new InvalidOperationException($"Distance function '{distanceFunction}' for {nameof(VectorStoreVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB for MongoDB VectorStore.") + _ => throw new NotSupportedException($"Distance function '{distanceFunction}' for {nameof(VectorStoreVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB for MongoDB VectorStore.") }; } diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj index 921efdecdd4d..52b98d55b40e 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj @@ -15,9 +15,6 @@ - - BsonValueFactory.cs - diff --git a/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoDistanceFunctionTests.cs b/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoDistanceFunctionTests.cs index 7cf58f65da31..6a4409ae7b12 100644 --- a/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoDistanceFunctionTests.cs +++ b/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoDistanceFunctionTests.cs @@ -10,7 +10,7 @@ namespace CosmosMongoDB.ConformanceTests; public class CosmosMongoDistanceFunctionTests(CosmosMongoDistanceFunctionTests.Fixture fixture) : DistanceFunctionTests(fixture), IClassFixture { - public override Task CosineDistance() => Assert.ThrowsAsync(base.CosineDistance); + public override Task CosineSimilarity() => Assert.ThrowsAsync(base.CosineSimilarity); public override Task EuclideanSquaredDistance() => Assert.ThrowsAsync(base.EuclideanSquaredDistance); public override Task NegativeDotProductSimilarity() => Assert.ThrowsAsync(base.NegativeDotProductSimilarity); public override Task HammingDistance() => Assert.ThrowsAsync(base.HammingDistance); diff --git a/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoIndexKindTests.cs b/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoIndexKindTests.cs index 421005184060..af6c091c4dab 100644 --- a/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoIndexKindTests.cs +++ b/dotnet/test/VectorData/CosmosMongoDB.ConformanceTests/CosmosMongoIndexKindTests.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using CosmosMongoDB.ConformanceTests.Support; +using Microsoft.Extensions.VectorData; using VectorData.ConformanceTests; using VectorData.ConformanceTests.Support; +using VectorData.ConformanceTests.Xunit; using Xunit; namespace CosmosMongoDB.ConformanceTests; @@ -10,6 +12,18 @@ namespace CosmosMongoDB.ConformanceTests; public class CosmosMongoIndexKindTests(CosmosMongoIndexKindTests.Fixture fixture) : IndexKindTests(fixture), IClassFixture { + // Note: Cosmos Mongo support HNSW, but only in a specific tier. + // [ConditionalFact] + // public virtual Task Hnsw() + // => this.Test(IndexKind.Hnsw); + + [ConditionalFact] + public virtual Task IvfFlat() + => this.Test(IndexKind.IvfFlat); + + // Cosmos Mongo does not support index-less searching + public override Task Flat() => Assert.ThrowsAsync(base.Flat); + public new class Fixture() : IndexKindTests.Fixture { public override TestStore TestStore => CosmosMongoTestStore.Instance; From 0e14af9db2e6bcdff91535f4b9713909e5c63f9f Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 8 Dec 2025 16:12:20 +0000 Subject: [PATCH 15/16] Revert "Add alternative wait strategy for MongoDB container." This reverts commit 0915d94a9f5d3dcb46df28fea00d7eacdb50863b. --- .../Support/MongoTestStore.cs | 61 +++---------------- 1 file changed, 10 insertions(+), 51 deletions(-) diff --git a/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs b/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs index 2b901f6bb73d..3a7c3fef820d 100644 --- a/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs +++ b/dotnet/test/VectorData/MongoDB.ConformanceTests/Support/MongoTestStore.cs @@ -19,8 +19,8 @@ internal sealed class MongoTestStore : TestStore private MongoDbContainer? _container; - private MongoClient? _client; - private IMongoDatabase? _database; + public MongoClient? _client { get; private set; } + public IMongoDatabase? _database { get; private set; } public MongoClient Client => this._client ?? throw new InvalidOperationException("Not initialized"); public IMongoDatabase Database => this._database ?? throw new InvalidOperationException("Not initialized"); @@ -46,8 +46,8 @@ protected override async Task StartAsync() private async Task StartMongoDbContainerAsync() { this._container = new MongoDbBuilder() - .WithImage("mongodb/mongodb-atlas-local:latest") - .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitForVectorIndexService())) + .WithImage("mongodb/mongodb-atlas-local:7.0.6") + .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new MongoDbWaitUntil())) .Build(); using CancellationTokenSource cts = new(); @@ -84,56 +84,15 @@ protected override async Task StopAsync() } } - private sealed class WaitForVectorIndexService : IWaitUntil + private sealed class MongoDbWaitUntil : IWaitUntil { + /// public async Task UntilAsync(IContainer container) { - var connectionString = $"mongodb://{container.Hostname}:{container.GetMappedPublicPort(27017).ToString()}?directConnection=true"; - using var client = new MongoClient(connectionString); - var databaseName = Guid.NewGuid().ToString(); - var weGood = false; - - try - { - var database = client.GetDatabase(databaseName); - var collectionName = Guid.NewGuid().ToString(); - await database.CreateCollectionAsync(collectionName); - - var model = new CreateSearchIndexModel( - Guid.NewGuid().ToString(), - SearchIndexType.VectorSearch, - BsonDocument.Parse( - """ - { - "fields": [ - { - "type": "vector", - "path": "Dummy", - "numDimensions": 8, - "similarity": "cosine" - } - ] - } - """)); - - await database.GetCollection(collectionName).SearchIndexes.CreateOneAsync(model); - weGood = true; - } - catch - { - // Intentionally ignored. - } - - try - { - await client.DropDatabaseAsync(databaseName); - } - catch - { - // Intentionally ignored. - } - - return weGood; + var (stdout, _) = await container.GetLogsAsync(timestampsEnabled: false) + .ConfigureAwait(false); + + return stdout.Contains("\"msg\":\"Waiting for connections\""); } } } From ed5c898610bcaddd9983576688c1d7c00b5ce614 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 12 Dec 2025 21:29:13 +0100 Subject: [PATCH 16/16] Fix tiny name simplification --- .../connectors/Memory/MongoDB/BsonValueFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/BsonValueFactory.cs b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/BsonValueFactory.cs index 07e0508db462..6787d9e07166 100644 --- a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/BsonValueFactory.cs +++ b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/BsonValueFactory.cs @@ -22,9 +22,9 @@ public static BsonValue Create(object? value) { null => BsonNull.Value, Guid guid => new BsonBinaryData(guid, GuidRepresentation.Standard), - Object[] array => new BsonArray(Array.ConvertAll(array, Create)), + object[] array => new BsonArray(Array.ConvertAll(array, Create)), Array array => new BsonArray(array), - IEnumerable enumerable => new BsonArray(enumerable.Select(Create)), + IEnumerable enumerable => new BsonArray(enumerable.Select(Create)), _ => BsonValue.Create(value) }; }