// Copyright (c) Microsoft. All rights reserved. using System.Linq.Expressions; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.VectorData; using VectorData.ConformanceTests.Support; using Xunit; namespace VectorData.ConformanceTests; // The type argument object? might not be serializable, which may cause Test Explorer to not enumerate individual data rows. Consider using a type that is known to be serializable. #pragma warning disable xUnit1045 public abstract class DependencyInjectionTests : DependencyInjectionTests where TVectorStore : VectorStore where TCollection : VectorStoreCollection where TKey : notnull where TRecord : class { protected virtual string CollectionName => "collectionName"; protected abstract void PopulateConfiguration(ConfigurationManager configuration, object? serviceKey = null); public abstract IEnumerable> StoreDelegates { get; } public abstract IEnumerable> CollectionDelegates { get; } [Fact] public void ServiceCollectionCantBeNull() { foreach (var registrationDelegate in this.StoreDelegates) { Assert.Throws(() => registrationDelegate(null!, null, ServiceLifetime.Singleton)); Assert.Throws(() => registrationDelegate(null!, "serviceKey", ServiceLifetime.Singleton)); } foreach (var registrationDelegate in this.CollectionDelegates) { Assert.Throws(() => registrationDelegate(null!, null, this.CollectionName, ServiceLifetime.Singleton)); Assert.Throws(() => registrationDelegate(null!, "serviceKey", this.CollectionName, ServiceLifetime.Singleton)); } } [Fact] public void CollectionNameCantBeNullOrEmpty() { const string EmptyCollectionName = ""; foreach (var registrationDelegate in this.CollectionDelegates) { IServiceCollection services = this.CreateServices(); Assert.Throws(() => registrationDelegate(services, null, null!, ServiceLifetime.Singleton)); Assert.Throws(() => registrationDelegate(services, "serviceKey", null!, ServiceLifetime.Singleton)); Assert.Throws(() => registrationDelegate(services, null, EmptyCollectionName, ServiceLifetime.Singleton)); Assert.Throws(() => registrationDelegate(services, "serviceKey", EmptyCollectionName, ServiceLifetime.Singleton)); } } [Theory, MemberData(nameof(LifetimesAndServiceKeys))] public virtual void CanRegisterVectorStore(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in this.StoreDelegates) { IServiceCollection services = this.CreateServices(serviceKey); registrationDelegate(services, serviceKey, lifetime); using ServiceProvider serviceProvider = services.BuildServiceProvider(); // let's ensure that concrete types are registered Verify(serviceProvider, lifetime, serviceKey); // and the abstraction too Verify(serviceProvider, lifetime, serviceKey); } } [Theory, MemberData(nameof(LifetimesAndServiceKeys))] public void CanRegisterCollections(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in this.CollectionDelegates) { IServiceCollection services = this.CreateServices(serviceKey); registrationDelegate(services, serviceKey, this.CollectionName, lifetime); using ServiceProvider serviceProvider = services.BuildServiceProvider(); // Let's ensure that concrete types are registered. Verify(serviceProvider, lifetime, serviceKey); // And the VectorStoreCollection abstraction. Verify>(serviceProvider, lifetime, serviceKey); // And the IVectorSearchable abstraction. Verify>(serviceProvider, lifetime, serviceKey); if (typeof(IKeywordHybridSearchable).IsAssignableFrom(typeof(TCollection))) { Verify>(serviceProvider, lifetime, serviceKey); } else { Assert.Null(serviceProvider.GetService>()); } } } [Theory, MemberData(nameof(LifetimesAndServiceKeys))] public virtual void CanRegisterConcreteTypeVectorStoreAfterSomeAbstractionHasBeenRegistered(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in this.StoreDelegates) { IServiceCollection services = this.CreateServices(serviceKey); // Users may be willing to register more than one IVectorStore implementation. services.Add(new ServiceDescriptor(typeof(VectorStore), serviceKey, (sp, key) => new FakeVectorStore(), lifetime)); registrationDelegate(services, serviceKey, lifetime); using ServiceProvider serviceProvider = services.BuildServiceProvider(); // let's ensure that concrete types are registered Verify(serviceProvider, lifetime, serviceKey); } } [Theory, MemberData(nameof(LifetimesAndServiceKeys))] public void CanRegisterConcreteTypeCollectionsAfterSomeAbstractionHasBeenRegistered(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in this.CollectionDelegates) { IServiceCollection services = this.CreateServices(serviceKey); // Users may be willing to register more than one VectorStoreCollection implementation. services.Add(new ServiceDescriptor(typeof(VectorStoreCollection), serviceKey, (sp, key) => new FakeVectorStoreRecordCollection(), lifetime)); registrationDelegate(services, serviceKey, this.CollectionName, lifetime); using ServiceProvider serviceProvider = services.BuildServiceProvider(); // let's ensure that concrete types are registered Verify(serviceProvider, lifetime, serviceKey); } } [Theory, MemberData(nameof(LifetimesAndServiceKeys))] public void EmbeddingGeneratorIsResolved(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in this.CollectionDelegates) { IServiceCollection services = this.CreateServices(serviceKey); bool wasResolved = false; services.AddSingleton(sp => { wasResolved = true; return null!; }); registrationDelegate(services, serviceKey, this.CollectionName, lifetime); Assert.False(wasResolved); // it's lazy using ServiceProvider serviceProvider = services.BuildServiceProvider(); using var collection = serviceKey is null ? serviceProvider.GetRequiredService() : serviceProvider.GetRequiredKeyedService(serviceKey); Assert.True(wasResolved); } } #pragma warning disable CA1000 // Do not declare static members on generic types public static TheoryData LifetimesAndServiceKeys { get; } = GetLifetimesAndServiceKeys(); #pragma warning restore CA1000 // Do not declare static members on generic types private static TheoryData GetLifetimesAndServiceKeys() { TheoryData result = []; foreach (ServiceLifetime lifetime in new ServiceLifetime[] { ServiceLifetime.Scoped, ServiceLifetime.Singleton, ServiceLifetime.Transient }) { result.Add(lifetime, null); result.Add(lifetime, "key"); result.Add(lifetime, 8); } return result; } protected IServiceCollection CreateServices(object? serviceKey = null) { IServiceCollection services = new ServiceCollection(); #pragma warning disable CA2000 // Dispose objects before losing scope ConfigurationManager configuration = new(); #pragma warning restore CA2000 // Dispose objects before losing scope services.AddSingleton(configuration); this.PopulateConfiguration(configuration, serviceKey); return services; } private static void Verify(ServiceProvider serviceProvider, ServiceLifetime lifetime, object? serviceKey) where TService : class { TService? serviceFromFirstScope, serviceFromSecondScope, secondServiceFromSecondScope; using (IServiceScope scope1 = serviceProvider.CreateScope()) { serviceFromFirstScope = Resolve(scope1.ServiceProvider, serviceKey); } using (IServiceScope scope2 = serviceProvider.CreateScope()) { serviceFromSecondScope = Resolve(scope2.ServiceProvider, serviceKey); secondServiceFromSecondScope = Resolve(scope2.ServiceProvider, serviceKey); } Assert.NotNull(serviceFromFirstScope); Assert.NotNull(serviceFromSecondScope); Assert.NotNull(secondServiceFromSecondScope); switch (lifetime) { case ServiceLifetime.Singleton: Assert.Same(serviceFromFirstScope, serviceFromSecondScope); Assert.Same(serviceFromSecondScope, secondServiceFromSecondScope); break; case ServiceLifetime.Scoped: Assert.NotSame(serviceFromFirstScope, serviceFromSecondScope); Assert.Same(serviceFromSecondScope, secondServiceFromSecondScope); break; case ServiceLifetime.Transient: Assert.NotSame(serviceFromFirstScope, serviceFromSecondScope); Assert.NotSame(serviceFromSecondScope, secondServiceFromSecondScope); break; } } protected static string CreateConfigKey(string prefix, object? serviceKey, string suffix) => serviceKey is null ? $"{prefix}:{suffix}" : $"{prefix}:{serviceKey}:{suffix}"; private static TService Resolve(IServiceProvider serviceProvider, object? serviceKey = null) where TService : notnull => serviceKey is null ? serviceProvider.GetRequiredService() : serviceProvider.GetRequiredKeyedService(serviceKey); private sealed class FakeVectorStore : VectorStore { public override Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override Task EnsureCollectionDeletedAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override VectorStoreCollection GetCollection(string name, VectorStoreCollectionDefinition? definition = null) => throw new NotImplementedException(); public override VectorStoreCollection> GetDynamicCollection(string name, VectorStoreCollectionDefinition? definition = null) => throw new NotImplementedException(); public override object? GetService(Type serviceType, object? serviceKey = null) => throw new NotImplementedException(); public override IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } private sealed class FakeVectorStoreRecordCollection : VectorStoreCollection { public override string Name => throw new NotImplementedException(); public override Task CollectionExistsAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override Task EnsureCollectionExistsAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override Task EnsureCollectionDeletedAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override Task GetAsync(TKey key, RecordRetrievalOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override IAsyncEnumerable GetAsync(Expression> filter, int top, FilteredRecordRetrievalOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override object? GetService(Type serviceType, object? serviceKey = null) => throw new NotImplementedException(); public override IAsyncEnumerable> SearchAsync(TInput searchValue, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override Task UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } } public abstract class DependencyInjectionTests { public sealed class Record : TestRecord { [VectorStoreData(StorageName = "number")] public int Number { get; set; } [VectorStoreVector(Dimensions: 3, StorageName = "embedding")] public ReadOnlyMemory Floats { get; set; } } }