From 0bb4a5841648704b459bb7ca8fbc54ec35f7c914 Mon Sep 17 00:00:00 2001 From: Empiree Date: Wed, 28 Oct 2026 18:50:49 +0100 Subject: [PATCH 1/2] Cosmos, DocumentDB, PgVector: resolve embedding generator per DI scope --- MEVD/src/AzureCosmosDB/AzureCosmosDB.csproj | 2 +- .../CosmosServiceCollectionExtensions.cs | 14 +-- .../AzureDocumentDB/AzureDocumentDB.csproj | 2 +- .../DocumentDBServiceCollectionExtensions.cs | 12 +-- MEVD/src/PgVector/PgVector.csproj | 2 +- .../PostgresServiceCollectionExtensions.cs | 4 +- .../AzureCosmosDB.UnitTests.csproj | 1 + .../CosmosServiceCollectionExtensionsTests.cs | 84 ++++++++++++++++ .../AzureDocumentDB.UnitTests.csproj | 4 + ...umentDBServiceCollectionExtensionsTests.cs | 98 +++++++++++++++++++ .../PgVector.UnitTests.csproj | 4 + ...ostgresServiceCollectionExtensionsTests.cs | 46 +++++++++ 12 files changed, 255 insertions(+), 18 deletions(-) create mode 100644 MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs create mode 100644 MEVD/test/AzureDocumentDB.UnitTests/DocumentDBServiceCollectionExtensionsTests.cs create mode 100644 MEVD/test/PgVector.UnitTests/PostgresServiceCollectionExtensionsTests.cs diff --git a/MEVD/src/AzureCosmosDB/AzureCosmosDB.csproj b/MEVD/src/AzureCosmosDB/AzureCosmosDB.csproj index 56f2a4f..9acc9b8 100644 --- a/MEVD/src/AzureCosmosDB/AzureCosmosDB.csproj +++ b/MEVD/src/AzureCosmosDB/AzureCosmosDB.csproj @@ -1,6 +1,6 @@ - 1.0.1 + 1.0.2 CommunityToolkit.VectorData.AzureCosmosDB $(AssemblyName) net10.0;net8.0;netstandard2.0;net462 diff --git a/MEVD/src/AzureCosmosDB/CosmosServiceCollectionExtensions.cs b/MEVD/src/AzureCosmosDB/CosmosServiceCollectionExtensions.cs index 1ba4980..c5026fe 100644 --- a/MEVD/src/AzureCosmosDB/CosmosServiceCollectionExtensions.cs +++ b/MEVD/src/AzureCosmosDB/CosmosServiceCollectionExtensions.cs @@ -57,9 +57,9 @@ public static IServiceCollection AddKeyedCosmosVectorStore( services.Add(new ServiceDescriptor(typeof(CosmosVectorStore), serviceKey, (sp, _) => { var database = sp.GetRequiredService(); - options = GetStoreOptions(sp, _ => options); + var copy = GetStoreOptions(sp, _ => options); - return new CosmosVectorStore(database, options); + return new CosmosVectorStore(database, copy); }, lifetime)); services.Add(new ServiceDescriptor(typeof(VectorStore), serviceKey, @@ -110,10 +110,10 @@ public static IServiceCollection AddKeyedCosmosVectorStore( services.Add(new ServiceDescriptor(typeof(CosmosVectorStore), serviceKey, (sp, _) => { - options = GetStoreOptions(sp, _ => options); - var clientOptions = CreateClientOptions(options?.JsonSerializerOptions); + var copy = GetStoreOptions(sp, _ => options); + var clientOptions = CreateClientOptions(copy?.JsonSerializerOptions); - return new CosmosVectorStore(connectionString, databaseName, clientOptions, options); + return new CosmosVectorStore(connectionString, databaseName, clientOptions, copy); }, lifetime)); services.Add(new ServiceDescriptor(typeof(VectorStore), serviceKey, @@ -167,9 +167,9 @@ public static IServiceCollection AddKeyedCosmosCollection( services.Add(new ServiceDescriptor(typeof(CosmosCollection), serviceKey, (sp, _) => { var database = sp.GetRequiredService(); - options = GetCollectionOptions(sp, _ => options); + var copy = GetCollectionOptions(sp, _ => options); - return new CosmosCollection(database, name, options); + return new CosmosCollection(database, name, copy); }, lifetime)); AddAbstractions(services, serviceKey, lifetime); diff --git a/MEVD/src/AzureDocumentDB/AzureDocumentDB.csproj b/MEVD/src/AzureDocumentDB/AzureDocumentDB.csproj index 244c119..1d19227 100644 --- a/MEVD/src/AzureDocumentDB/AzureDocumentDB.csproj +++ b/MEVD/src/AzureDocumentDB/AzureDocumentDB.csproj @@ -1,7 +1,7 @@  - 1.1.0 + 1.1.1 CommunityToolkit.VectorData.AzureDocumentDB $(AssemblyName) net10.0;net8.0;netstandard2.1;net472 diff --git a/MEVD/src/AzureDocumentDB/DocumentDBServiceCollectionExtensions.cs b/MEVD/src/AzureDocumentDB/DocumentDBServiceCollectionExtensions.cs index dee80ba..2cd0d42 100644 --- a/MEVD/src/AzureDocumentDB/DocumentDBServiceCollectionExtensions.cs +++ b/MEVD/src/AzureDocumentDB/DocumentDBServiceCollectionExtensions.cs @@ -54,9 +54,9 @@ public static IServiceCollection AddKeyedDocumentDBVectorStore( services.Add(new ServiceDescriptor(typeof(DocumentDBVectorStore), serviceKey, (sp, _) => { var database = sp.GetRequiredService(); - options = GetStoreOptions(sp, _ => options); + var copy = GetStoreOptions(sp, _ => options); - return new DocumentDBVectorStore(database, options); + return new DocumentDBVectorStore(database, copy); }, lifetime)); services.Add(new ServiceDescriptor(typeof(VectorStore), serviceKey, @@ -107,11 +107,11 @@ public static IServiceCollection AddKeyedDocumentDBVectorStore( services.Add(new ServiceDescriptor(typeof(DocumentDBVectorStore), serviceKey, (sp, _) => { - options = GetStoreOptions(sp, _ => options); + var copy = GetStoreOptions(sp, _ => options); MongoClient mongoClient = new(CreateClientSettings(connectionString)); var database = mongoClient.GetDatabase(databaseName); - return new DocumentDBVectorStore(database, options); + return new DocumentDBVectorStore(database, copy); }, lifetime)); services.Add(new ServiceDescriptor(typeof(VectorStore), serviceKey, @@ -162,9 +162,9 @@ public static IServiceCollection AddKeyedDocumentDBCollection( services.Add(new ServiceDescriptor(typeof(DocumentDBCollection), serviceKey, (sp, _) => { var database = sp.GetRequiredService(); - options = GetCollectionOptions(sp, _ => options); + var copy = GetCollectionOptions(sp, _ => options); - return new DocumentDBCollection(database, name, options); + return new DocumentDBCollection(database, name, copy); }, lifetime)); AddAbstractions(services, serviceKey, lifetime); diff --git a/MEVD/src/PgVector/PgVector.csproj b/MEVD/src/PgVector/PgVector.csproj index 2d9e8ab..04c76b0 100644 --- a/MEVD/src/PgVector/PgVector.csproj +++ b/MEVD/src/PgVector/PgVector.csproj @@ -1,7 +1,7 @@  - 1.0.1 + 1.0.2 CommunityToolkit.VectorData.PgVector $(AssemblyName) net10.0;net8.0;netstandard2.0;net462 diff --git a/MEVD/src/PgVector/PostgresServiceCollectionExtensions.cs b/MEVD/src/PgVector/PostgresServiceCollectionExtensions.cs index 84afd6c..b69e104 100644 --- a/MEVD/src/PgVector/PostgresServiceCollectionExtensions.cs +++ b/MEVD/src/PgVector/PostgresServiceCollectionExtensions.cs @@ -35,10 +35,10 @@ public static IServiceCollection AddPostgresVectorStore( services.Add(new ServiceDescriptor(typeof(PostgresVectorStore), (sp) => { var dataSource = sp.GetRequiredService(); - options = GetStoreOptions(sp, _ => options); + var copy = GetStoreOptions(sp, _ => options); // The data source has been solved from the DI container, so we do not own it. - return new PostgresVectorStore(dataSource, ownsDataSource: false, options); + return new PostgresVectorStore(dataSource, ownsDataSource: false, copy); }, lifetime)); services.Add(new ServiceDescriptor(typeof(VectorStore), diff --git a/MEVD/test/AzureCosmosDB.UnitTests/AzureCosmosDB.UnitTests.csproj b/MEVD/test/AzureCosmosDB.UnitTests/AzureCosmosDB.UnitTests.csproj index 0ff8ba9..fce3fbe 100644 --- a/MEVD/test/AzureCosmosDB.UnitTests/AzureCosmosDB.UnitTests.csproj +++ b/MEVD/test/AzureCosmosDB.UnitTests/AzureCosmosDB.UnitTests.csproj @@ -19,6 +19,7 @@ + diff --git a/MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs b/MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..a9811ac --- /dev/null +++ b/MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using CommunityToolkit.VectorData.UnitTests; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.VectorData; +using Moq; +using Xunit; + +namespace CommunityToolkit.VectorData.AzureCosmosDB.UnitTests; + +public sealed class CosmosServiceCollectionExtensionsTests +{ + [Theory] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void VectorStoreGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifetime) + { + int resolvedGenerators = 0; + ServiceCollection services = CreateServices(() => resolvedGenerators++); + services.AddCosmosVectorStore(lifetime: lifetime); + + ResolveInTwoScopes(services); + + Assert.Equal(2, resolvedGenerators); + } + + [Theory] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void CollectionGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifetime) + { + int resolvedGenerators = 0; + ServiceCollection services = CreateServices(() => resolvedGenerators++); + services.AddCosmosCollection("collection", lifetime: lifetime); + + ResolveInTwoScopes>(services); + + Assert.Equal(2, resolvedGenerators); + } + + private static ServiceCollection CreateServices(Action onGeneratorResolved) + { + Mock client = new(); + client.SetupGet(c => c.ClientOptions).Returns(new CosmosClientOptions { UseSystemTextJsonSerializerWithOptions = new() }); + Mock database = new(); + database.SetupGet(d => d.Client).Returns(client.Object); + database.Setup(d => d.GetContainer(It.IsAny())).Returns(new Mock().Object); + + ServiceCollection services = new(); + services.AddSingleton(database.Object); + services.AddScoped(_ => + { + onGeneratorResolved(); + return new FakeEmbeddingGenerator(); + }); + + return services; + } + + private static void ResolveInTwoScopes(ServiceCollection services) + where TService : notnull + { + using ServiceProvider serviceProvider = services.BuildServiceProvider(); + + for (int i = 0; i < 2; i++) + { + using IServiceScope scope = serviceProvider.CreateScope(); + scope.ServiceProvider.GetRequiredService(); + } + } + + public sealed class Record + { + [VectorStoreKey] + public string Id { get; set; } = string.Empty; + + [VectorStoreVector(3)] + public ReadOnlyMemory Embedding { get; set; } + } +} diff --git a/MEVD/test/AzureDocumentDB.UnitTests/AzureDocumentDB.UnitTests.csproj b/MEVD/test/AzureDocumentDB.UnitTests/AzureDocumentDB.UnitTests.csproj index bd59fc3..52425af 100644 --- a/MEVD/test/AzureDocumentDB.UnitTests/AzureDocumentDB.UnitTests.csproj +++ b/MEVD/test/AzureDocumentDB.UnitTests/AzureDocumentDB.UnitTests.csproj @@ -20,6 +20,10 @@ + + + + diff --git a/MEVD/test/AzureDocumentDB.UnitTests/DocumentDBServiceCollectionExtensionsTests.cs b/MEVD/test/AzureDocumentDB.UnitTests/DocumentDBServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..a84417b --- /dev/null +++ b/MEVD/test/AzureDocumentDB.UnitTests/DocumentDBServiceCollectionExtensionsTests.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using CommunityToolkit.VectorData.AzureDocumentDB; +using CommunityToolkit.VectorData.UnitTests; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using MongoDB.Driver; +using Moq; +using Xunit; + +namespace AzureDocumentDB.UnitTests; + +public sealed class DocumentDBServiceCollectionExtensionsTests +{ + [Theory] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void VectorStoreGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifetime) + { + // Arrange + var resolvedGenerators = 0; + var services = CreateServices(() => resolvedGenerators++); + services.AddDocumentDBVectorStore(lifetime: lifetime); + + // Act + ResolveInTwoScopes(services); + + // Assert + Assert.Equal(2, resolvedGenerators); + } + + [Theory] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void VectorStoreWithConnectionStringGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifetime) + { + // Arrange + var resolvedGenerators = 0; + var services = CreateServices(() => resolvedGenerators++); + services.AddDocumentDBVectorStore("mongodb://localhost", "database", lifetime: lifetime); + + // Act + ResolveInTwoScopes(services); + + // Assert + Assert.Equal(2, resolvedGenerators); + } + + [Theory] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void CollectionGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifetime) + { + // Arrange + var resolvedGenerators = 0; + var services = CreateServices(() => resolvedGenerators++); + services.AddDocumentDBCollection("collection", lifetime: lifetime); + + // Act + ResolveInTwoScopes>(services); + + // Assert + Assert.Equal(2, resolvedGenerators); + } + + private static ServiceCollection CreateServices(Action onGeneratorResolved) + { + var database = new Mock(); + database + .Setup(d => d.GetCollection(It.IsAny(), It.IsAny())) + .Returns(Mock.Of>()); + + ServiceCollection services = new(); + services.AddSingleton(database.Object); + services.AddScoped(_ => + { + onGeneratorResolved(); + return new FakeEmbeddingGenerator(); + }); + + return services; + } + + private static void ResolveInTwoScopes(ServiceCollection services) + where TService : notnull + { + using var serviceProvider = services.BuildServiceProvider(); + + for (var i = 0; i < 2; i++) + { + using var scope = serviceProvider.CreateScope(); + scope.ServiceProvider.GetRequiredService(); + } + } +} diff --git a/MEVD/test/PgVector.UnitTests/PgVector.UnitTests.csproj b/MEVD/test/PgVector.UnitTests/PgVector.UnitTests.csproj index dc7a091..1292ab6 100644 --- a/MEVD/test/PgVector.UnitTests/PgVector.UnitTests.csproj +++ b/MEVD/test/PgVector.UnitTests/PgVector.UnitTests.csproj @@ -21,6 +21,10 @@ + + + + diff --git a/MEVD/test/PgVector.UnitTests/PostgresServiceCollectionExtensionsTests.cs b/MEVD/test/PgVector.UnitTests/PostgresServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..39615a7 --- /dev/null +++ b/MEVD/test/PgVector.UnitTests/PostgresServiceCollectionExtensionsTests.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using CommunityToolkit.VectorData.PgVector; +using CommunityToolkit.VectorData.UnitTests; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Npgsql; +using Xunit; + +namespace PgVector.UnitTests; + +public sealed class PostgresServiceCollectionExtensionsTests +{ + [Theory] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void VectorStoreGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifetime) + { + // Arrange + var resolvedGenerators = 0; + using var dataSource = NpgsqlDataSource.Create("Host=localhost;Database=test;"); + + var services = new ServiceCollection(); + services.AddSingleton(dataSource); + services.AddScoped(_ => + { + resolvedGenerators++; + return new FakeEmbeddingGenerator(); + }); + services.AddPostgresVectorStore(lifetime: lifetime); + + // Act + using (var serviceProvider = services.BuildServiceProvider()) + { + for (var i = 0; i < 2; i++) + { + using var scope = serviceProvider.CreateScope(); + scope.ServiceProvider.GetRequiredService(); + } + } + + // Assert + Assert.Equal(2, resolvedGenerators); + } +} From d32b0e7b0767b2b2139c45bfec85e8907fcc3b5d Mon Sep 17 00:00:00 2001 From: Empiree Date: Wed, 28 Oct 2026 20:53:55 +0100 Subject: [PATCH 2/2] Cosmos: add test for the connection string overload --- .../CosmosServiceCollectionExtensionsTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs b/MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs index a9811ac..b4828be 100644 --- a/MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs +++ b/MEVD/test/AzureCosmosDB.UnitTests/CosmosServiceCollectionExtensionsTests.cs @@ -14,6 +14,8 @@ namespace CommunityToolkit.VectorData.AzureCosmosDB.UnitTests; public sealed class CosmosServiceCollectionExtensionsTests { + private const string ConnectionString = "AccountEndpoint=https://localhost:8081;AccountKey=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; + [Theory] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient)] @@ -28,6 +30,20 @@ public void VectorStoreGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifet Assert.Equal(2, resolvedGenerators); } + [Theory] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void VectorStoreWithConnectionStringGetsEmbeddingGeneratorFromEachScope(ServiceLifetime lifetime) + { + int resolvedGenerators = 0; + ServiceCollection services = CreateServices(() => resolvedGenerators++); + services.AddCosmosVectorStore(ConnectionString, "database", lifetime: lifetime); + + ResolveInTwoScopes(services); + + Assert.Equal(2, resolvedGenerators); + } + [Theory] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient)]