chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,6 @@
# Suppressing errors for Test projects under dotnet folder
[*.cs]
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
@@ -0,0 +1,709 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.MongoDB;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Moq;
using Xunit;
using MEVD = Microsoft.Extensions.VectorData;
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
/// <summary>
/// Unit tests for <see cref="MongoCollection{TKey, TRecord}"/> class.
/// </summary>
public sealed class MongoCollectionTests
{
private readonly Mock<IMongoDatabase> _mockMongoDatabase = new();
private readonly Mock<IMongoCollection<BsonDocument>> _mockMongoCollection = new();
public MongoCollectionTests()
{
this._mockMongoDatabase
.Setup(l => l.GetCollection<BsonDocument>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
.Returns(this._mockMongoCollection.Object);
}
[Fact]
public void ConstructorForModelWithoutKeyThrowsException()
{
// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => new MongoCollection<string, object>(this._mockMongoDatabase.Object, "collection"));
Assert.Contains("No key property found", exception.Message);
}
[Fact]
public void ConstructorWithDeclarativeModelInitializesCollection()
{
// Act & Assert
using var collection = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithImperativeModelInitializesCollection()
{
// Arrange
var definition = new VectorStoreCollectionDefinition
{
Properties = [new VectorStoreKeyProperty("Id", typeof(string))]
};
// Act
using var collection = new MongoCollection<string, TestModel>(
this._mockMongoDatabase.Object,
"collection",
new() { Definition = definition });
// Assert
Assert.NotNull(collection);
}
[Theory]
[MemberData(nameof(CollectionExistsData))]
public async Task CollectionExistsReturnsValidResultAsync(List<string> collections, string collectionName, bool expectedResult)
{
// Arrange
var mockCursor = new Mock<IAsyncCursor<string>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns(collections);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
collectionName);
// Act
var actualResult = await sut.CollectionExistsAsync();
// Assert
Assert.Equal(expectedResult, actualResult);
}
[Fact]
public async Task EnsureCollectionExistsInvokesValidMethodsAsync()
{
// Arrange
const string CollectionName = "collection";
var mockCursor = new Mock<IAsyncCursor<string>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([]);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
var mockIndexCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockIndexCursor
.SetupSequence(l => l.MoveNext(It.IsAny<CancellationToken>()))
.Returns(true)
.Returns(false);
mockIndexCursor
.Setup(l => l.Current)
.Returns([]);
var mockMongoIndexManager = new Mock<IMongoIndexManager<BsonDocument>>();
mockMongoIndexManager
.Setup(l => l.ListAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockIndexCursor.Object);
this._mockMongoCollection
.Setup(l => l.Indexes)
.Returns(mockMongoIndexManager.Object);
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionExistsAsync();
// Assert
this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync(
CollectionName,
It.IsAny<CreateCollectionOptions>(),
It.IsAny<CancellationToken>()), Times.Exactly(1));
this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync(
It.IsAny<ListCollectionNamesOptions>(),
It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task DeleteInvokesValidMethodsAsync()
{
// Arrange
const string RecordKey = "key";
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], RecordKey);
// Act
await sut.DeleteAsync(RecordKey);
// Assert
this._mockMongoCollection.Verify(l => l.DeleteOneAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DeleteBatchInvokesValidMethodsAsync()
{
// Arrange
List<string> recordKeys = ["key1", "key2"];
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.In(document => document["_id"].AsString, recordKeys);
// Act
await sut.DeleteAsync(recordKeys);
// Assert
this._mockMongoCollection.Verify(l => l.DeleteManyAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DeleteCollectionInvokesValidMethodsAsync()
{
// Arrange
const string CollectionName = "collection";
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionDeletedAsync();
// Assert
this._mockMongoDatabase.Verify(l => l.DropCollectionAsync(
It.Is<string>(name => name == CollectionName),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
var document = new BsonDocument { ["_id"] = RecordKey, ["HotelName"] = "Test Name" };
var mockCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([document]);
this._mockMongoCollection
.Setup(l => l.FindAsync(
It.IsAny<FilterDefinition<BsonDocument>>(),
It.IsAny<FindOptions<BsonDocument>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
var result = await sut.GetAsync(RecordKey);
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.HotelId);
Assert.Equal("Test Name", result.HotelName);
}
[Fact]
public async Task GetBatchReturnsValidRecordAsync()
{
// Arrange
var document1 = new BsonDocument { ["_id"] = "key1", ["HotelName"] = "Test Name 1" };
var document2 = new BsonDocument { ["_id"] = "key2", ["HotelName"] = "Test Name 2" };
var document3 = new BsonDocument { ["_id"] = "key3", ["HotelName"] = "Test Name 3" };
var mockCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockCursor
.SetupSequence(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true)
.ReturnsAsync(false);
mockCursor
.Setup(l => l.Current)
.Returns([document1, document2, document3]);
this._mockMongoCollection
.Setup(l => l.FindAsync(
It.IsAny<FilterDefinition<BsonDocument>>(),
It.IsAny<FindOptions<BsonDocument>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
var results = await sut.GetAsync(["key1", "key2", "key3"]).ToListAsync();
// Assert
Assert.NotNull(results[0]);
Assert.Equal("key1", results[0].HotelId);
Assert.Equal("Test Name 1", results[0].HotelName);
Assert.NotNull(results[1]);
Assert.Equal("key2", results[1].HotelId);
Assert.Equal("Test Name 2", results[1].HotelName);
Assert.NotNull(results[2]);
Assert.Equal("key3", results[2].HotelId);
Assert.Equal("Test Name 3", results[2].HotelName);
}
[Fact]
public async Task CanUpsertRecordAsync()
{
// Arrange
var hotel = new MongoHotelModel("key") { HotelName = "Test Name" };
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], "key");
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
await sut.UpsertAsync(hotel);
// Assert
this._mockMongoCollection.Verify(l => l.ReplaceOneAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.Is<BsonDocument>(document =>
document["_id"] == "key" &&
document["HotelName"] == "Test Name"),
It.IsAny<ReplaceOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task CanUpsertManyRecordsAsync()
{
// Arrange
var hotel1 = new MongoHotelModel("key1") { HotelName = "Test Name 1" };
var hotel2 = new MongoHotelModel("key2") { HotelName = "Test Name 2" };
var hotel3 = new MongoHotelModel("key3") { HotelName = "Test Name 3" };
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
await sut.UpsertAsync([hotel1, hotel2, hotel3]);
}
[Fact]
public async Task UpsertWithModelWorksCorrectlyAsync()
{
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty("Id", typeof(string)),
new VectorStoreDataProperty("HotelName", typeof(string))
]
};
await this.TestUpsertWithModelAsync<TestModel>(
dataModel: new TestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "HotelName",
definition: definition);
}
[Fact]
public async Task UpsertWithVectorStoreModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync<VectorStoreTestModel>(
dataModel: new VectorStoreTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "HotelName");
}
[Fact]
public async Task UpsertWithBsonModelWorksCorrectlyAsync()
{
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty("Id", typeof(string)),
new VectorStoreDataProperty("HotelName", typeof(string))
]
};
await this.TestUpsertWithModelAsync<BsonTestModel>(
dataModel: new BsonTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "hotel_name",
definition: definition);
}
[Fact]
public async Task UpsertWithBsonVectorStoreModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync<BsonVectorStoreTestModel>(
dataModel: new BsonVectorStoreTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "hotel_name");
}
[Fact]
public async Task UpsertWithBsonVectorStoreWithNameModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync<BsonVectorStoreWithNameTestModel>(
dataModel: new BsonVectorStoreWithNameTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "bson_hotel_name");
}
[Theory]
[MemberData(nameof(SearchVectorTypeData))]
public async Task SearchThrowsExceptionWithInvalidVectorTypeAsync(object vector, bool exceptionExpected)
{
// Arrange
this.MockCollectionForSearch();
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act & Assert
if (exceptionExpected)
{
await Assert.ThrowsAsync<NotSupportedException>(async () => await sut.SearchAsync(vector, top: 3).ToListAsync());
}
else
{
Assert.NotNull(await sut.SearchAsync(vector, top: 3).FirstOrDefaultAsync());
}
}
[Theory]
[InlineData("TestEmbedding1", "TestEmbedding1", 3, 3)]
[InlineData("TestEmbedding2", "test_embedding_2", 4, 4)]
public async Task SearchUsesValidQueryAsync(
string? vectorPropertyName,
string expectedVectorPropertyName,
int actualTop,
int expectedTop)
{
// Arrange
var vector = new ReadOnlyMemory<float>([1f, 2f, 3f]);
var expectedSearch = new BsonDocument
{
{ "$vectorSearch",
new BsonDocument
{
{ "index", "vector_index" },
{ "queryVector", BsonArray.Create(vector.ToArray()) },
{ "path", expectedVectorPropertyName },
{ "limit", expectedTop },
{ "numCandidates", expectedTop * 10 },
}
}
};
var expectedProjection = new BsonDocument
{
{ "$project",
new BsonDocument
{
{ "similarityScore", new BsonDocument { { "$meta", "vectorSearchScore" } } },
{ "document", "$$ROOT" }
}
}
};
this.MockCollectionForSearch();
using var sut = new MongoCollection<string, VectorSearchModel>(
this._mockMongoDatabase.Object,
"collection");
Expression<Func<VectorSearchModel, object?>>? vectorSelector = vectorPropertyName switch
{
"TestEmbedding1" => record => record.TestEmbedding1,
"TestEmbedding2" => record => record.TestEmbedding2,
_ => null
};
// Act
var actual = await sut.SearchAsync(vector, top: actualTop, new()
{
VectorProperty = vectorSelector,
}).FirstOrDefaultAsync();
// Assert
Assert.NotNull(actual);
this._mockMongoCollection.Verify(l => l.AggregateAsync(
It.Is<PipelineDefinition<BsonDocument, BsonDocument>>(pipeline =>
this.ComparePipeline(pipeline, expectedSearch, expectedProjection)),
It.IsAny<AggregateOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SearchThrowsExceptionWithNonExistentVectorPropertyNameAsync()
{
// Arrange
this.MockCollectionForSearch();
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
var options = new MEVD.VectorSearchOptions<MongoHotelModel> { VectorProperty = r => "non-existent-property" };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3, options).FirstOrDefaultAsync());
}
[Fact]
public async Task SearchReturnsRecordWithScoreAsync()
{
// Arrange
this.MockCollectionForSearch();
using var sut = new MongoCollection<string, MongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
var result = await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3).FirstOrDefaultAsync();
// Assert
Assert.NotNull(result);
Assert.Equal("key", result.Record.HotelId);
Assert.Equal("Test Name", result.Record.HotelName);
Assert.Equal(0.99f, result.Score);
}
public static TheoryData<List<string>, string, bool> CollectionExistsData => new()
{
{ ["collection-2"], "collection-2", true },
{ [], "non-existent-collection", false }
};
public static TheoryData<List<string>, int> EnsureCollectionExistsData => new()
{
{ ["collection"], 0 },
{ [], 1 }
};
public static TheoryData<object, bool> SearchVectorTypeData => new()
{
{ new ReadOnlyMemory<float>([1f, 2f, 3f]), false },
{ new ReadOnlyMemory<float>?(new([1f, 2f, 3f])), false },
{ new List<float>([1f, 2f, 3f]), true },
};
#region private
private bool ComparePipeline(
PipelineDefinition<BsonDocument, BsonDocument> actualPipeline,
BsonDocument expectedSearch,
BsonDocument expectedProjection)
{
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var documents = actualPipeline.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry)).Documents;
return
documents[0].ToJson() == expectedSearch.ToJson() &&
documents[1].ToJson() == expectedProjection.ToJson();
}
private void MockCollectionForSearch()
{
var document = new BsonDocument { ["_id"] = "key", ["HotelName"] = "Test Name" };
var searchResult = new BsonDocument { ["document"] = document, ["similarityScore"] = 0.99f };
var mockCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([searchResult]);
this._mockMongoCollection
.Setup(l => l.AggregateAsync(
It.IsAny<PipelineDefinition<BsonDocument, BsonDocument>>(),
It.IsAny<AggregateOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
}
private async Task TestUpsertWithModelAsync<TDataModel>(
TDataModel dataModel,
string expectedPropertyName,
VectorStoreCollectionDefinition? definition = null)
where TDataModel : class
{
// Arrange
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], "key");
MongoCollectionOptions? options = definition != null ?
new() { Definition = definition } :
null;
using var sut = new MongoCollection<string, TDataModel>(
this._mockMongoDatabase.Object,
"collection",
options);
// Act
await sut.UpsertAsync(dataModel);
// Assert
this._mockMongoCollection.Verify(l => l.ReplaceOneAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.Is<BsonDocument>(document =>
document["_id"] == "key" &&
document.Contains(expectedPropertyName) &&
document[expectedPropertyName] == "Test Name"),
It.IsAny<ReplaceOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
private static bool CompareFilterDefinitions(
FilterDefinition<BsonDocument> actual,
FilterDefinition<BsonDocument> expected,
IBsonSerializer<BsonDocument> documentSerializer,
IBsonSerializerRegistry serializerRegistry)
{
return actual.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry)) ==
expected.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry));
}
#pragma warning disable CA1812
private sealed class TestModel
{
public string? Id { get; set; }
public string? HotelName { get; set; }
}
private sealed class VectorStoreTestModel
{
[VectorStoreKey]
public string? Id { get; set; }
[VectorStoreData(StorageName = "hotel_name")]
public string? HotelName { get; set; }
}
private sealed class BsonTestModel
{
[BsonId]
public string? Id { get; set; }
[BsonElement("hotel_name")]
public string? HotelName { get; set; }
}
private sealed class BsonVectorStoreTestModel
{
[BsonId]
[VectorStoreKey]
public string? Id { get; set; }
[BsonElement("hotel_name")]
[VectorStoreData]
public string? HotelName { get; set; }
}
private sealed class BsonVectorStoreWithNameTestModel
{
[BsonId]
[VectorStoreKey]
public string? Id { get; set; }
[BsonElement("bson_hotel_name")]
[VectorStoreData(StorageName = "storage_hotel_name")]
public string? HotelName { get; set; }
}
private sealed class VectorSearchModel
{
[BsonId]
[VectorStoreKey]
public string? Id { get; set; }
[VectorStoreData]
public string? HotelName { get; set; }
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.IvfFlat, StorageName = "test_embedding_1")]
public ReadOnlyMemory<float> TestEmbedding1 { get; set; }
[BsonElement("test_embedding_2")]
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.IvfFlat)]
public ReadOnlyMemory<float> TestEmbedding2 { get; set; }
}
#pragma warning restore CA1812
#endregion
}
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>SemanticKernel.Connectors.MongoDB.UnitTests</AssemblyName>
<RootNamespace>SemanticKernel.Connectors.MongoDB.UnitTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);SKEXP0001,VSTHRD111,CA2007,CS1591</NoWarn>
<NoWarn>$(NoWarn);MEVD9001</NoWarn> <!-- Experimental MEVD connector-facing APIs -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\VectorData\MongoDB\MongoDB.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,280 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.VectorData;
using Microsoft.Extensions.VectorData.ProviderServices;
using Microsoft.SemanticKernel.Connectors.MongoDB;
using MongoDB.Bson;
using Xunit;
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
/// <summary>
/// Unit tests for <see cref="MongoDynamicMapper"/> class.
/// </summary>
public sealed class MongoDynamicMapperTests
{
private static readonly CollectionModel s_model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("BoolDataProp", typeof(bool)),
new VectorStoreDataProperty("NullableBoolDataProp", typeof(bool?)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreDataProperty("IntDataProp", typeof(int)),
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
new VectorStoreDataProperty("LongDataProp", typeof(long)),
new VectorStoreDataProperty("NullableLongDataProp", typeof(long?)),
new VectorStoreDataProperty("FloatDataProp", typeof(float)),
new VectorStoreDataProperty("NullableFloatDataProp", typeof(float?)),
new VectorStoreDataProperty("DoubleDataProp", typeof(double)),
new VectorStoreDataProperty("NullableDoubleDataProp", typeof(double?)),
new VectorStoreDataProperty("DecimalDataProp", typeof(decimal)),
new VectorStoreDataProperty("NullableDecimalDataProp", typeof(decimal?)),
new VectorStoreDataProperty("DateTimeDataProp", typeof(DateTime)),
new VectorStoreDataProperty("NullableDateTimeDataProp", typeof(DateTime?)),
new VectorStoreDataProperty("TagListDataProp", typeof(List<string>)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
]);
private static readonly float[] s_floatVector = [1.0f, 2.0f, 3.0f];
private static readonly List<string> s_taglist = ["tag1", "tag2"];
[Fact]
public void MapFromDataToStorageModelMapsAllSupportedTypes()
{
// Arrange
var sut = new MongoDynamicMapper(s_model);
var dataModel = new Dictionary<string, object?>
{
["Key"] = "key",
["BoolDataProp"] = true,
["NullableBoolDataProp"] = false,
["StringDataProp"] = "string",
["IntDataProp"] = 1,
["NullableIntDataProp"] = 2,
["LongDataProp"] = 3L,
["NullableLongDataProp"] = 4L,
["FloatDataProp"] = 5.0f,
["NullableFloatDataProp"] = 6.0f,
["DoubleDataProp"] = 7.0,
["NullableDoubleDataProp"] = 8.0,
["DecimalDataProp"] = 9.0m,
["NullableDecimalDataProp"] = 10.0m,
["DateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
["NullableDateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
["TagListDataProp"] = s_taglist,
["FloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
["NullableFloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
};
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Equal("key", storageModel["_id"]);
Assert.Equal(true, (bool?)storageModel["BoolDataProp"]);
Assert.Equal(false, (bool?)storageModel["NullableBoolDataProp"]);
Assert.Equal("string", (string?)storageModel["StringDataProp"]);
Assert.Equal(1, (int?)storageModel["IntDataProp"]);
Assert.Equal(2, (int?)storageModel["NullableIntDataProp"]);
Assert.Equal(3L, (long?)storageModel["LongDataProp"]);
Assert.Equal(4L, (long?)storageModel["NullableLongDataProp"]);
Assert.Equal(5.0f, (float?)storageModel["FloatDataProp"].AsDouble);
Assert.Equal(6.0f, (float?)storageModel["NullableFloatDataProp"].AsNullableDouble);
Assert.Equal(7.0, (double?)storageModel["DoubleDataProp"]);
Assert.Equal(8.0, (double?)storageModel["NullableDoubleDataProp"]);
Assert.Equal(9.0m, (decimal?)storageModel["DecimalDataProp"]);
Assert.Equal(10.0m, (decimal?)storageModel["NullableDecimalDataProp"]);
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), storageModel["DateTimeDataProp"].ToUniversalTime());
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), storageModel["NullableDateTimeDataProp"].ToUniversalTime());
Assert.Equal(s_taglist, storageModel["TagListDataProp"]!.AsBsonArray.Select(x => (string)x!).ToArray());
Assert.Equal(s_floatVector, storageModel["FloatVector"]!.AsBsonArray.Select(x => (float)x.AsDouble!).ToArray());
Assert.Equal(s_floatVector, storageModel["NullableFloatVector"]!.AsBsonArray.Select(x => (float)x.AsNullableDouble!).ToArray());
}
[Fact]
public void MapFromDataToStorageModelMapsNullValues()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
]);
var dataModel = new Dictionary<string, object?>
{
["Key"] = "key",
["StringDataProp"] = null,
["NullableIntDataProp"] = null,
["NullableFloatVector"] = null
};
var sut = new MongoDynamicMapper(model);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Equal(BsonNull.Value, storageModel["StringDataProp"]);
Assert.Equal(BsonNull.Value, storageModel["NullableIntDataProp"]);
Assert.Empty(storageModel["NullableFloatVector"].AsBsonArray);
}
[Fact]
public void MapFromStorageToDataModelMapsAllSupportedTypes()
{
// Arrange
var sut = new MongoDynamicMapper(s_model);
var storageModel = new BsonDocument
{
["_id"] = "key",
["BoolDataProp"] = true,
["NullableBoolDataProp"] = false,
["StringDataProp"] = "string",
["IntDataProp"] = 1,
["NullableIntDataProp"] = 2,
["LongDataProp"] = 3L,
["NullableLongDataProp"] = 4L,
["FloatDataProp"] = 5.0f,
["NullableFloatDataProp"] = 6.0f,
["DoubleDataProp"] = 7.0,
["NullableDoubleDataProp"] = 8.0,
["DecimalDataProp"] = 9.0m,
["NullableDecimalDataProp"] = 10.0m,
["DateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
["NullableDateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
["TagListDataProp"] = BsonArray.Create(s_taglist),
["FloatVector"] = BsonArray.Create(s_floatVector),
["NullableFloatVector"] = BsonArray.Create(s_floatVector),
};
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.Equal(true, dataModel["BoolDataProp"]);
Assert.Equal(false, dataModel["NullableBoolDataProp"]);
Assert.Equal("string", dataModel["StringDataProp"]);
Assert.Equal(1, dataModel["IntDataProp"]);
Assert.Equal(2, dataModel["NullableIntDataProp"]);
Assert.Equal(3L, dataModel["LongDataProp"]);
Assert.Equal(4L, dataModel["NullableLongDataProp"]);
Assert.Equal(5.0f, dataModel["FloatDataProp"]);
Assert.Equal(6.0f, dataModel["NullableFloatDataProp"]);
Assert.Equal(7.0, dataModel["DoubleDataProp"]);
Assert.Equal(8.0, dataModel["NullableDoubleDataProp"]);
Assert.Equal(9.0m, dataModel["DecimalDataProp"]);
Assert.Equal(10.0m, dataModel["NullableDecimalDataProp"]);
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), dataModel["DateTimeDataProp"]);
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), dataModel["NullableDateTimeDataProp"]);
Assert.Equal(s_taglist, dataModel["TagListDataProp"]);
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["NullableFloatVector"]!)!.ToArray());
}
[Fact]
public void MapFromStorageToDataModelMapsNullValues()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
]);
var storageModel = new BsonDocument
{
["_id"] = "key",
["StringDataProp"] = BsonNull.Value,
["NullableIntDataProp"] = BsonNull.Value,
["NullableFloatVector"] = BsonNull.Value
};
var sut = new MongoDynamicMapper(model);
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.Null(dataModel["StringDataProp"]);
Assert.Null(dataModel["NullableIntDataProp"]);
Assert.Null(dataModel["NullableFloatVector"]);
}
[Fact]
public void MapFromStorageToDataModelThrowsForMissingKey()
{
// Arrange
var sut = new MongoDynamicMapper(s_model);
var storageModel = new BsonDocument();
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => sut.MapFromStorageToDataModel(storageModel, includeVectors: true));
}
[Fact]
public void MapFromDataToStorageModelSkipsMissingProperties()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
]);
var dataModel = new Dictionary<string, object?> { ["Key"] = "key" };
var sut = new MongoDynamicMapper(model);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Equal("key", (string?)storageModel["_id"]);
Assert.False(storageModel.Contains("StringDataProp"));
Assert.False(storageModel.Contains("FloatVector"));
}
[Fact]
public void MapFromStorageToDataModelSkipsMissingProperties()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
]);
var storageModel = new BsonDocument
{
["_id"] = "key"
};
var sut = new MongoDynamicMapper(model);
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.False(dataModel.ContainsKey("StringDataProp"));
Assert.False(dataModel.ContainsKey("FloatVector"));
}
private static CollectionModel BuildModel(List<VectorStoreProperty> properties)
=> new MongoModelBuilder().BuildDynamic(new() { Properties = properties }, defaultEmbeddingGenerator: null);
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.VectorData;
using MongoDB.Bson.Serialization.Attributes;
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
public class MongoHotelModel(string hotelId)
{
/// <summary>The key of the record.</summary>
[VectorStoreKey]
public string HotelId { get; init; } = hotelId;
/// <summary>A string metadata field.</summary>
[VectorStoreData(IsIndexed = true)]
public string? HotelName { get; set; }
/// <summary>An int metadata field.</summary>
[VectorStoreData]
public int HotelCode { get; set; }
/// <summary>A float metadata field.</summary>
[VectorStoreData]
public float? HotelRating { get; set; }
/// <summary>A bool metadata field.</summary>
[BsonElement("parking_is_included")]
[VectorStoreData]
public bool ParkingIncluded { get; set; }
/// <summary>An array metadata field.</summary>
[VectorStoreData]
public List<string> Tags { get; set; } = [];
/// <summary>A data field.</summary>
[VectorStoreData]
public string? Description { get; set; }
/// <summary>A vector field.</summary>
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineSimilarity)]
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
}
@@ -0,0 +1,88 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.MongoDB;
using MongoDB.Bson;
using Xunit;
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
/// <summary>
/// Unit tests for <see cref="MongoMapper{TRecord}"/> class.
/// </summary>
public sealed class MongoMapperTests
{
private readonly MongoMapper<MongoHotelModel> _sut;
public MongoMapperTests()
{
var keyProperty = new VectorStoreKeyProperty("HotelId", typeof(string));
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
keyProperty,
new VectorStoreDataProperty("HotelName", typeof(string)),
new VectorStoreDataProperty("Tags", typeof(List<string>)),
new VectorStoreDataProperty("ParkingIncluded", typeof(bool)),
new VectorStoreVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory<float>?), 10)
]
};
this._sut = new(new MongoModelBuilder().Build(typeof(MongoHotelModel), typeof(string), definition, defaultEmbeddingGenerator: null));
}
[Fact]
public void MapFromDataToStorageModelReturnsValidObject()
{
// Arrange
var hotel = new MongoHotelModel("key")
{
HotelName = "Test Name",
Tags = ["tag1", "tag2"],
ParkingIncluded = true,
DescriptionEmbedding = new ReadOnlyMemory<float>([1f, 2f, 3f])
};
// Act
var document = this._sut.MapFromDataToStorageModel(hotel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.NotNull(document);
Assert.Equal("key", document["_id"]);
Assert.Equal("Test Name", document["HotelName"]);
Assert.Equal(["tag1", "tag2"], document["Tags"].AsBsonArray);
Assert.True(document["parking_is_included"].AsBoolean);
Assert.Equal([1f, 2f, 3f], document["DescriptionEmbedding"].AsBsonArray);
}
[Fact]
public void MapFromStorageToDataModelReturnsValidObject()
{
// Arrange
var document = new BsonDocument
{
["_id"] = "key",
["HotelName"] = "Test Name",
["Tags"] = BsonArray.Create(new List<string> { "tag1", "tag2" }),
["parking_is_included"] = BsonValue.Create(true),
["DescriptionEmbedding"] = BsonArray.Create(new List<float> { 1f, 2f, 3f })
};
// Act
var hotel = this._sut.MapFromStorageToDataModel(document, includeVectors: true);
// Assert
Assert.NotNull(hotel);
Assert.Equal("key", hotel.HotelId);
Assert.Equal("Test Name", hotel.HotelName);
Assert.Equal(["tag1", "tag2"], hotel.Tags);
Assert.True(hotel.ParkingIncluded);
Assert.True(new ReadOnlyMemory<float>([1f, 2f, 3f]).Span.SequenceEqual(hotel.DescriptionEmbedding!.Value.Span));
}
}
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Connectors.MongoDB;
using MongoDB.Driver;
using Moq;
using Xunit;
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
/// <summary>
/// Unit tests for <see cref="MongoVectorStore"/> class.
/// </summary>
public sealed class MongoVectorStoreTests
{
private readonly Mock<IMongoDatabase> _mockMongoDatabase = new();
[Fact]
public void GetCollectionWithNotSupportedKeyThrowsException()
{
// Arrange
using var sut = new MongoVectorStore(this._mockMongoDatabase.Object);
// Act & Assert
Assert.Throws<NotSupportedException>(() => sut.GetCollection<byte[], MongoHotelModel>("collection"));
}
[Fact]
public void GetCollectionWithoutFactoryReturnsDefaultCollection()
{
// Arrange
using var sut = new MongoVectorStore(this._mockMongoDatabase.Object);
// Act
var collection = sut.GetCollection<string, MongoHotelModel>("collection");
// Assert
Assert.NotNull(collection);
}
[Fact]
public async Task ListCollectionNamesReturnsCollectionNamesAsync()
{
// Arrange
var expectedCollectionNames = new List<string> { "collection-1", "collection-2", "collection-3" };
var mockCursor = new Mock<IAsyncCursor<string>>();
mockCursor
.SetupSequence(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true)
.ReturnsAsync(false);
mockCursor
.Setup(l => l.Current)
.Returns(expectedCollectionNames);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new MongoVectorStore(this._mockMongoDatabase.Object);
// Act
var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync();
// Assert
Assert.Equal(expectedCollectionNames, actualCollectionNames);
}
}