File: DependencyInjectionTests.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.VectorData.ConformanceTests\Microsoft.Extensions.VectorData.ConformanceTests.csproj (Microsoft.Extensions.VectorData.ConformanceTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
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
// File may only contain a single type
#pragma warning disable SA1402
 
public abstract class DependencyInjectionTests<TVectorStore, TCollection, TKey, TRecord> : DependencyInjectionTests<TKey>
    where TVectorStore : VectorStore
    where TCollection : VectorStoreCollection<TKey, TRecord>
    where TKey : notnull
    where TRecord : class
{
    protected virtual string CollectionName => "collectionName";
 
    protected abstract void PopulateConfiguration(ConfigurationManager configuration, object? serviceKey = null);
 
    public abstract IEnumerable<Func<IServiceCollection, object?, ServiceLifetime, IServiceCollection>> StoreDelegates { get; }
 
    public abstract IEnumerable<Func<IServiceCollection, object?, string, ServiceLifetime, IServiceCollection>> CollectionDelegates { get; }
 
    [Fact]
    public virtual void ServiceCollectionCantBeNull()
    {
        foreach (var registrationDelegate in StoreDelegates)
        {
            Assert.Throws<ArgumentNullException>(() => registrationDelegate(null!, null, ServiceLifetime.Singleton));
            Assert.Throws<ArgumentNullException>(() => registrationDelegate(null!, "serviceKey", ServiceLifetime.Singleton));
        }
 
        foreach (var registrationDelegate in CollectionDelegates)
        {
            Assert.Throws<ArgumentNullException>(() => registrationDelegate(null!, null, CollectionName, ServiceLifetime.Singleton));
            Assert.Throws<ArgumentNullException>(() => registrationDelegate(null!, "serviceKey", CollectionName, ServiceLifetime.Singleton));
        }
    }
 
    [Fact]
    public virtual void CollectionNameCantBeNullOrEmpty()
    {
        const string EmptyCollectionName = "";
 
        foreach (var registrationDelegate in CollectionDelegates)
        {
            IServiceCollection services = CreateServices();
 
            Assert.Throws<ArgumentNullException>(() => registrationDelegate(services, null, null!, ServiceLifetime.Singleton));
            Assert.Throws<ArgumentNullException>(() => registrationDelegate(services, "serviceKey", null!, ServiceLifetime.Singleton));
            Assert.Throws<ArgumentException>(() => registrationDelegate(services, null, EmptyCollectionName, ServiceLifetime.Singleton));
            Assert.Throws<ArgumentException>(() => registrationDelegate(services, "serviceKey", EmptyCollectionName, ServiceLifetime.Singleton));
        }
    }
 
    [Theory]
    [MemberData(nameof(LifetimesAndServiceKeys))]
    public virtual void CanRegisterVectorStore(ServiceLifetime lifetime, object? serviceKey)
    {
        foreach (var registrationDelegate in StoreDelegates)
        {
            IServiceCollection services = CreateServices(serviceKey);
 
            registrationDelegate(services, serviceKey, lifetime);
 
            using ServiceProvider serviceProvider = services.BuildServiceProvider();
 
            // let's ensure that concrete types are registered
            Verify<TVectorStore>(serviceProvider, lifetime, serviceKey);
 
            // and the abstraction too
            Verify<VectorStore>(serviceProvider, lifetime, serviceKey);
        }
    }
 
    [Theory]
    [MemberData(nameof(LifetimesAndServiceKeys))]
    public virtual void CanRegisterCollections(ServiceLifetime lifetime, object? serviceKey)
    {
        foreach (var registrationDelegate in CollectionDelegates)
        {
            IServiceCollection services = CreateServices(serviceKey);
 
            registrationDelegate(services, serviceKey, CollectionName, lifetime);
 
            using ServiceProvider serviceProvider = services.BuildServiceProvider();
 
            // Let's ensure that concrete types are registered.
            Verify<TCollection>(serviceProvider, lifetime, serviceKey);
 
            // And the VectorStoreCollection abstraction.
            Verify<VectorStoreCollection<TKey, TRecord>>(serviceProvider, lifetime, serviceKey);
 
            // And the IVectorSearchable abstraction.
            Verify<IVectorSearchable<TRecord>>(serviceProvider, lifetime, serviceKey);
 
            if (typeof(IKeywordHybridSearchable<TRecord>).IsAssignableFrom(typeof(TCollection)))
            {
                Verify<IKeywordHybridSearchable<TRecord>>(serviceProvider, lifetime, serviceKey);
            }
            else
            {
                Assert.Null(serviceProvider.GetService<IKeywordHybridSearchable<TRecord>>());
            }
        }
    }
 
    [Theory]
    [MemberData(nameof(LifetimesAndServiceKeys))]
    public virtual void CanRegisterConcreteTypeVectorStoreAfterSomeAbstractionHasBeenRegistered(ServiceLifetime lifetime, object? serviceKey)
    {
        foreach (var registrationDelegate in StoreDelegates)
        {
            IServiceCollection services = 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<TVectorStore>(serviceProvider, lifetime, serviceKey);
        }
    }
 
    [Theory]
    [MemberData(nameof(LifetimesAndServiceKeys))]
    public virtual void CanRegisterConcreteTypeCollectionsAfterSomeAbstractionHasBeenRegistered(ServiceLifetime lifetime, object? serviceKey)
    {
        foreach (var registrationDelegate in CollectionDelegates)
        {
            IServiceCollection services = CreateServices(serviceKey);
 
            // Users may be willing to register more than one VectorStoreCollection implementation.
            services.Add(new ServiceDescriptor(typeof(VectorStoreCollection<TKey, TRecord>), serviceKey, (sp, key) => new FakeVectorStoreRecordCollection(), lifetime));
 
            registrationDelegate(services, serviceKey, CollectionName, lifetime);
 
            using ServiceProvider serviceProvider = services.BuildServiceProvider();
 
            // let's ensure that concrete types are registered
            Verify<TCollection>(serviceProvider, lifetime, serviceKey);
        }
    }
 
    [Theory]
    [MemberData(nameof(LifetimesAndServiceKeys))]
    public virtual void EmbeddingGeneratorIsResolved(ServiceLifetime lifetime, object? serviceKey)
    {
        foreach (var registrationDelegate in CollectionDelegates)
        {
            IServiceCollection services = CreateServices(serviceKey);
 
            bool wasResolved = false;
            services.AddSingleton<IEmbeddingGenerator>(sp =>
            {
                wasResolved = true;
                return null!;
            });
 
            registrationDelegate(services, serviceKey, CollectionName, lifetime);
 
            Assert.False(wasResolved); // it's lazy
 
            using ServiceProvider serviceProvider = services.BuildServiceProvider();
            using var collection = serviceKey is null
                ? serviceProvider.GetRequiredService<TCollection>()
                : serviceProvider.GetRequiredKeyedService<TCollection>(serviceKey);
 
            Assert.True(wasResolved);
        }
    }
 
#pragma warning disable CA1000 // Do not declare static members on generic types
    public static TheoryData<ServiceLifetime, object?> LifetimesAndServiceKeys { get; } = GetLifetimesAndServiceKeys();
#pragma warning restore CA1000 // Do not declare static members on generic types
 
    private static TheoryData<ServiceLifetime, object?> GetLifetimesAndServiceKeys()
    {
        TheoryData<ServiceLifetime, object?> 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<IConfiguration>(configuration);
 
        PopulateConfiguration(configuration, serviceKey);
 
        return services;
    }
 
    private static void Verify<TService>(ServiceProvider serviceProvider, ServiceLifetime lifetime, object? serviceKey)
        where TService : class
    {
        TService? serviceFromFirstScope;
        TService? serviceFromSecondScope;
        TService? secondServiceFromSecondScope;
 
        using (IServiceScope scope1 = serviceProvider.CreateScope())
        {
            serviceFromFirstScope = Resolve<TService>(scope1.ServiceProvider, serviceKey);
        }
 
        using (IServiceScope scope2 = serviceProvider.CreateScope())
        {
            serviceFromSecondScope = Resolve<TService>(scope2.ServiceProvider, serviceKey);
 
            secondServiceFromSecondScope = Resolve<TService>(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<TService>(IServiceProvider serviceProvider, object? serviceKey = null)
        where TService : notnull
        => serviceKey is null
            ? serviceProvider.GetRequiredService<TService>()
            : serviceProvider.GetRequiredKeyedService<TService>(serviceKey);
 
    private sealed class FakeVectorStore : VectorStore
    {
        public override Task<bool> CollectionExistsAsync(string name, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override Task EnsureCollectionDeletedAsync(string name, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override VectorStoreCollection<TKey1, TRecord1> GetCollection<TKey1, TRecord1>(string name, VectorStoreCollectionDefinition? definition = null)
            => throw new NotSupportedException();
        public override VectorStoreCollection<object, Dictionary<string, object?>> GetDynamicCollection(string name, VectorStoreCollectionDefinition? definition)
            => throw new NotSupportedException();
        public override object? GetService(Type serviceType, object? serviceKey = null)
            => throw new NotSupportedException();
        public override IAsyncEnumerable<string> ListCollectionNamesAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
    }
 
    private sealed class FakeVectorStoreRecordCollection : VectorStoreCollection<TKey, TRecord>
    {
        public override string Name
            => throw new NotSupportedException();
        public override Task<bool> CollectionExistsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override Task EnsureCollectionExistsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override Task DeleteAsync(TKey key, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override Task EnsureCollectionDeletedAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override Task<TRecord?> GetAsync(TKey key, RecordRetrievalOptions? options = null, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override IAsyncEnumerable<TRecord> GetAsync(
            Expression<Func<TRecord, bool>> filter,
            int top,
            FilteredRecordRetrievalOptions<TRecord>? options = null,
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override object? GetService(Type serviceType, object? serviceKey = null)
            => throw new NotSupportedException();
        public override IAsyncEnumerable<VectorSearchResult<TRecord>> SearchAsync<TInput>(
            TInput searchValue,
            int top,
            VectorSearchOptions<TRecord>? options = null,
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
        public override Task UpsertAsync(IEnumerable<TRecord> records, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
    }
}
 
public abstract class DependencyInjectionTests<TKey>
{
    public sealed class Record : TestRecord<TKey>
    {
        [VectorStoreData(StorageName = "number")]
        public int Number { get; set; }
 
        [VectorStoreVector(dimensions: 3, StorageName = "embedding")]
        public ReadOnlyMemory<float> Floats { get; set; }
    }
}