Files
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

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; }
}
}