Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<PackageVersion Include="DuckDB.NET.Data.Full" Version="1.2.0" />
<PackageVersion Include="DuckDB.NET.Data" Version="1.1.3" />
<PackageVersion Include="MongoDB.Driver" Version="2.30.0" />
<PackageVersion Include="MongoDB.Driver" Version="3.5.2" />
<PackageVersion Include="Microsoft.Graph" Version="5.94.0" />
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.24" />
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ public BsonDocument MapFromDataToStorageModel(Dictionary<string, object?> 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,

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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -40,7 +41,8 @@ public MongoMapper(CollectionModel model)

var conventionPack = new ConventionPack
{
new IgnoreExtraElementsConvention(ignoreExtraElements: true)
new IgnoreExtraElementsConvention(ignoreExtraElements: true),
new GuidStandardRepresentationConvention()
};

ConventionRegistry.Register(
Expand Down Expand Up @@ -139,4 +141,15 @@ public TRecord MapFromStorageToDataModel(BsonDocument storageModel, bool include

return BsonSerializer.Deserialize<TRecord>(storageModel);
}

private class GuidStandardRepresentationConvention : ConventionBase, IMemberMapConvention
{
public void Apply(BsonMemberMap memberMap)
{
if (memberMap.MemberType == typeof(Guid) && memberMap.MemberInfo.GetCustomAttribute<BsonRepresentationAttribute>() is null)
{
memberMap.SetSerializer(new GuidSerializer(GuidRepresentation.Standard));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class AllowNullAttribute : Attribute
Expand Down
4 changes: 2 additions & 2 deletions dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private static SocketsHttpHandler CreateHandler()
},
};
}
#elif NETSTANDARD2_0
#elif NETSTANDARD2_0_OR_GREATER
private static HttpClientHandler CreateHandler()
{
var handler = new HttpClientHandler();
Expand All @@ -99,7 +99,7 @@ private static HttpClientHandler CreateHandler()
catch (PlatformNotSupportedException) { } // not supported on older frameworks
return handler;
}
#elif NET462
#elif NETFRAMEWORK
private static HttpClientHandler CreateHandler()
=> new();
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion dotnet/src/VectorData/CosmosMongoDB/CosmosMongoDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
<AssemblyName>Microsoft.SemanticKernel.Connectors.CosmosMongoDB</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<TargetFrameworks>net10.0;net8.0;netstandard2.0;net462</TargetFrameworks>
<TargetFrameworks>net10.0;net8.0;netstandard2.1;net472</TargetFrameworks>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
<VersionSuffix>preview</VersionSuffix>
</PropertyGroup>
Expand All @@ -15,6 +15,9 @@

<ItemGroup>
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/*.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Include="..\MongoDB\BsonValueFactory.cs">
<Link>BsonValueFactory.cs</Link>
</Compile>
</ItemGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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))
}
};
}
Expand Down
27 changes: 27 additions & 0 deletions dotnet/src/VectorData/MongoDB/BsonValueFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using MongoDB.Bson;

namespace Microsoft.SemanticKernel.Connectors.MongoDB;

/// <summary>
/// A class that constructs the correct BsonValue for a given CLR type.
/// </summary>
internal static class BsonValueFactory
{
/// <summary>
/// Create a BsonValue for the given CLR type.
/// </summary>
/// <param name="value">The CLR object to create a BSON value for.</param>
/// <returns>The appropriate <see cref="BsonValue"/> for that CLR type.</returns>
public static BsonValue Create(object? value)
=> value switch
{
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)
};
}
42 changes: 40 additions & 2 deletions dotnet/src/VectorData/MongoDB/MongoCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -75,6 +76,9 @@ public class MongoCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRecor
/// <summary>Number of nearest neighbors to use during the vector search.</summary>
private readonly int? _numCandidates;

/// <summary><see cref="BsonSerializationInfo"/> to use for serializing key values.</summary>
private readonly BsonSerializationInfo? _keySerializationInfo;

/// <summary>Types of keys permitted.</summary>
private static readonly Type[] s_validKeyTypes = [typeof(string), typeof(Guid), typeof(ObjectId), typeof(int), typeof(long)];

Expand Down Expand Up @@ -135,6 +139,11 @@ internal MongoCollection(IMongoDatabase mongoDatabase, string name, Func<MongoCo
VectorStoreName = mongoDatabase.DatabaseNamespace?.DatabaseName,
CollectionName = name
};

// Cache the key serialization info if possible
this._keySerializationInfo = typeof(TKey) == typeof(object)
? null
: this.GetKeySerializationInfo();
}

/// <inheritdoc />
Expand Down Expand Up @@ -676,10 +685,39 @@ private async IAsyncEnumerable<VectorSearchResult<TRecord>> EnumerateAndMapSearc
}

private FilterDefinition<BsonDocument> GetFilterById(TKey id)
=> Builders<BsonDocument>.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, 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<BsonDocument>.Filter.Eq(MongoConstants.MongoReservedKeyPropertyName, bsonValue);
}

private FilterDefinition<BsonDocument> GetFilterByIds(IEnumerable<TKey> ids)
=> Builders<BsonDocument>.Filter.In(MongoConstants.MongoReservedKeyPropertyName, 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<BsonDocument>.Filter.In(MongoConstants.MongoReservedKeyPropertyName, bsonValues);
}

private BsonSerializationInfo GetKeySerializationInfo()
{
var documentSerializer = BsonSerializer.LookupSerializer<TRecord>();
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<bool> InternalCollectionExistsAsync(CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dotnet/src/VectorData/MongoDB/MongoDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
<AssemblyName>Microsoft.SemanticKernel.Connectors.MongoDB</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<TargetFrameworks>net10.0;net8.0;netstandard2.0;net462</TargetFrameworks>
<TargetFrameworks>net10.0;net8.0;netstandard2.1;net472</TargetFrameworks>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
<VersionSuffix>preview</VersionSuffix>
</PropertyGroup>
Expand Down
6 changes: 3 additions & 3 deletions dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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))
}
};
}
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A number of the conformance tests are failing for me, all with a similar error, e.g.
MongoRecordConformanceTests_Guid.GetAsync_WithVectors

Note that these tests have be enabled manually to be run by commenting out the following line in assembly info.
[assembly: DisableTests(Skip = "The MongoDB container is intermittently timing out at startup time blocking prs, so these test should be run manually.")]

Error:

Message: 
MongoDB.Bson.BsonSerializationException : An error occurred while serializing the Id property of class VectorData.ConformanceTests.Models.SimpleRecord`1[[System.Guid, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]: GuidSerializer cannot serialize a Guid when GuidRepresentation is Unspecified.
---- MongoDB.Bson.BsonSerializationException : GuidSerializer cannot serialize a Guid when GuidRepresentation is Unspecified.

Stack Trace: 
BsonClassMapSerializer1.SerializeMember(BsonSerializationContext context, Object obj, BsonMemberMap memberMap) BsonClassMapSerializer1.SerializeClass(BsonSerializationContext context, BsonSerializationArgs args, TClass document)
BsonClassMapSerializer1.Serialize(BsonSerializationContext context, BsonSerializationArgs args, TClass value) BsonExtensionMethods.ToBsonDocument(Object obj, Type nominalType, IBsonSerializer serializer, Action1 configurator, BsonSerializationArgs args)
BsonExtensionMethods.ToBsonDocument[TNominalType](TNominalType obj, IBsonSerializer1 serializer, Action1 configurator, BsonSerializationArgs args)
MongoMapper1.MapFromDataToStorageModel(TRecord dataModel, Int32 recordIndex, IReadOnlyList1[] generatedEmbeddings) line 53
MongoCollection2.UpsertCoreAsync(TRecord record, Int32 recordIndex, IReadOnlyList1[] generatedEmbeddings, CancellationToken cancellationToken) line 270
MongoCollection2.UpsertAsync(IEnumerable1 records, CancellationToken cancellationToken) line 261
VectorStoreCollectionFixture2.SeedAsync() line 52 VectorStoreCollectionFixture2.InitializeAsync() line 41
----- Inner Stack Trace -----
GuidSerializer.Serialize(BsonSerializationContext context, BsonSerializationArgs args, Guid value)
IBsonSerializer.Serialize(BsonSerializationContext context, BsonSerializationArgs args, Object value)
IBsonSerializerExtensions.Serialize(IBsonSerializer serializer, BsonSerializationContext context, Object value)
BsonClassMapSerializer1.SerializeNormalMember(BsonSerializationContext context, Object obj, BsonMemberMap memberMap) BsonClassMapSerializer1.SerializeMember(BsonSerializationContext context, Object obj, BsonMemberMap memberMap)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes made to MongoCollection would need to be applied to CosmosCollection. I don't have authorisation to work on that at my end nor access to a Cosmos instance/emulator to test it with. Sorry.

Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading