284 lines
14 KiB
C#
284 lines
14 KiB
C#
// 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<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 void ServiceCollectionCantBeNull()
|
|
{
|
|
foreach (var registrationDelegate in this.StoreDelegates)
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => registrationDelegate(null!, null, ServiceLifetime.Singleton));
|
|
Assert.Throws<ArgumentNullException>(() => registrationDelegate(null!, "serviceKey", ServiceLifetime.Singleton));
|
|
}
|
|
|
|
foreach (var registrationDelegate in this.CollectionDelegates)
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => registrationDelegate(null!, null, this.CollectionName, ServiceLifetime.Singleton));
|
|
Assert.Throws<ArgumentNullException>(() => 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<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 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<TVectorStore>(serviceProvider, lifetime, serviceKey);
|
|
// and the abstraction too
|
|
Verify<VectorStore>(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<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 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<TVectorStore>(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<TKey, TRecord>), 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<TCollection>(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<IEmbeddingGenerator>(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<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);
|
|
|
|
this.PopulateConfiguration(configuration, serviceKey);
|
|
|
|
return services;
|
|
}
|
|
|
|
private static void Verify<TService>(ServiceProvider serviceProvider, ServiceLifetime lifetime, object? serviceKey)
|
|
where TService : class
|
|
{
|
|
TService? serviceFromFirstScope, serviceFromSecondScope, 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 NotImplementedException();
|
|
public override Task EnsureCollectionDeletedAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
|
public override VectorStoreCollection<TKey1, TRecord1> GetCollection<TKey1, TRecord1>(string name, VectorStoreCollectionDefinition? definition = null) => throw new NotImplementedException();
|
|
public override VectorStoreCollection<object, Dictionary<string, object?>> GetDynamicCollection(string name, VectorStoreCollectionDefinition? definition = null) => throw new NotImplementedException();
|
|
public override object? GetService(Type serviceType, object? serviceKey = null) => throw new NotImplementedException();
|
|
public override IAsyncEnumerable<string> ListCollectionNamesAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
|
}
|
|
|
|
private sealed class FakeVectorStoreRecordCollection : VectorStoreCollection<TKey, TRecord>
|
|
{
|
|
public override string Name => throw new NotImplementedException();
|
|
public override Task<bool> 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<TRecord?> GetAsync(TKey key, RecordRetrievalOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
|
public override IAsyncEnumerable<TRecord> GetAsync(Expression<Func<TRecord, bool>> filter, int top, FilteredRecordRetrievalOptions<TRecord>? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
|
public override object? GetService(Type serviceType, object? serviceKey = null) => throw new NotImplementedException();
|
|
public override IAsyncEnumerable<VectorSearchResult<TRecord>> SearchAsync<TInput>(TInput searchValue, int top, VectorSearchOptions<TRecord>? 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<TRecord> records, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
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; }
|
|
}
|
|
}
|