.Net: [MEVD] Add validation for mismatched collection key (#13594)

Closes #11355
This commit is contained in:
Shay Rojansky
2026-02-26 11:19:03 +00:00
committed by GitHub
parent f59e395230
commit f077655fd6
38 changed files with 126 additions and 54 deletions
@@ -46,6 +46,8 @@ internal class MongoModelBuilder() : CollectionModelBuilder(s_validationOptions)
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(string) && type != typeof(int) && type != typeof(long) && type != typeof(Guid) && type != typeof(ObjectId))
@@ -72,7 +72,7 @@ public class AzureAISearchCollection<TKey, TRecord> : VectorStoreCollection<TKey
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(AzureAISearchDynamicCollection)))
: new AzureAISearchModelBuilder()
.Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator, options.JsonSerializerOptions ?? JsonSerializerOptions.Default),
.Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator, options.JsonSerializerOptions ?? JsonSerializerOptions.Default),
options)
{
}
@@ -18,6 +18,8 @@ internal class AzureAISearchDynamicModelBuilder() : CollectionModelBuilder(s_mod
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(string) && type != typeof(Guid))
@@ -22,6 +22,8 @@ internal class AzureAISearchModelBuilder() : CollectionJsonModelBuilder(s_modelB
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(string) && type != typeof(Guid))
@@ -90,7 +90,7 @@ public class CosmosMongoCollection<TKey, TRecord> : VectorStoreCollection<TKey,
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(CosmosMongoDynamicCollection)))
: new MongoModelBuilder().Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new MongoModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -124,7 +124,7 @@ public class CosmosNoSqlCollection<TKey, TRecord> : VectorStoreCollection<TKey,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(CosmosNoSqlDynamicCollection)))
: new CosmosNoSqlModelBuilder()
.Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator, options.JsonSerializerOptions ?? JsonSerializerOptions.Default),
.Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator, options.JsonSerializerOptions ?? JsonSerializerOptions.Default),
options)
{
}
@@ -22,6 +22,13 @@ internal class CosmosNoSqlModelBuilder() : CollectionJsonModelBuilder(s_modelBui
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
// CosmosNoSqlKey is a composite key type (document ID + partition key) that doesn't correspond to any single key property type;
// skip the base TKey-to-key-property validation when it's used.
if (this.KeyType != typeof(CosmosNoSqlKey))
{
base.ValidateKeyProperty(keyProperty);
}
// Note that the key property in Cosmos NoSQL refers to the document ID, not to the CosmosNoSqlKey structure which includes both
// the document ID and the partition key (and which is the generic TKey type parameter of the collection).
var type = keyProperty.Type;
@@ -56,7 +56,7 @@ public class InMemoryCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRe
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(InMemoryDynamicCollection)))
: new InMemoryModelBuilder().Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new InMemoryModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -19,6 +19,8 @@ internal class InMemoryModelBuilder() : CollectionModelBuilder(ValidationOptions
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
// All .NET types are supported by the InMemory provider, but we support auto-generation of keys only for GUIDs
if (keyProperty.IsAutoGenerated && keyProperty.Type != typeof(Guid))
{
@@ -99,7 +99,7 @@ public class MongoCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRecor
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(MongoDynamicCollection)))
: new MongoModelBuilder().Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new MongoModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -92,7 +92,7 @@ public class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRe
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(PostgresDynamicCollection)))
: new PostgresModelBuilder().Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new PostgresModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -26,6 +26,8 @@ internal class PostgresModelBuilder() : CollectionModelBuilder(PostgresModelBuil
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(short)
@@ -66,7 +66,7 @@ public class PineconeCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRe
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(PineconeDynamicCollection)))
: new PineconeModelBuilder().Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new PineconeModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -20,6 +20,8 @@ internal class PineconeModelBuilder() : CollectionModelBuilder(s_validationOptio
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(string) && type != typeof(Guid))
@@ -90,7 +90,7 @@ public class QdrantCollection<TKey, TRecord> : VectorStoreCollection<TKey, TReco
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(QdrantDynamicCollection)))
: new QdrantModelBuilder(options.HasNamedVectors).Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new QdrantModelBuilder(options.HasNamedVectors).Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -21,6 +21,8 @@ internal class QdrantModelBuilder(bool hasNamedVectors) : CollectionModelBuilder
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(ulong) && type != typeof(Guid))
@@ -76,7 +76,7 @@ public class RedisHashSetCollection<TKey, TRecord> : VectorStoreCollection<TKey,
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(RedisHashSetDynamicCollection)))
: new RedisModelBuilder(ModelBuildingOptions).Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new RedisModelBuilder(ModelBuildingOptions).Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -83,6 +83,7 @@ public class RedisJsonCollection<TKey, TRecord> : VectorStoreCollection<TKey, TR
: new RedisJsonModelBuilder(ModelBuildingOptions)
.Build(
typeof(TRecord),
typeof(TKey),
options.Definition,
options.EmbeddingGenerator,
options.JsonSerializerOptions ?? JsonSerializerOptions.Default),
@@ -19,6 +19,8 @@ internal class RedisJsonDynamicModelBuilder(CollectionModelBuildingOptions optio
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(string) && type != typeof(Guid))
@@ -20,6 +20,8 @@ internal class RedisJsonModelBuilder(CollectionModelBuildingOptions options) : C
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(string) && type != typeof(Guid))
@@ -22,6 +22,8 @@ internal class RedisModelBuilder(CollectionModelBuildingOptions options) : Colle
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(string) && type != typeof(Guid))
@@ -59,7 +59,7 @@ public class SqlServerCollection<TKey, TRecord>
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(SqlServerDynamicCollection)))
: new SqlServerModelBuilder().Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new SqlServerModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -26,6 +26,8 @@ internal class SqlServerModelBuilder() : CollectionModelBuilder(s_modelBuildingO
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(int) && type != typeof(long) && type != typeof(string) && type != typeof(Guid))
@@ -74,7 +74,7 @@ public class SqliteCollection<TKey, TRecord> : VectorStoreCollection<TKey, TReco
name,
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(SqliteDynamicCollection)))
: new SqliteModelBuilder().Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator),
: new SqliteModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
options)
{
}
@@ -22,6 +22,8 @@ internal class SqliteModelBuilder() : CollectionModelBuilder(s_modelBuildingOpti
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
var type = keyProperty.Type;
if (type != typeof(int) && type != typeof(long) && type != typeof(string) && type != typeof(Guid))
@@ -27,19 +27,20 @@ public abstract class CollectionJsonModelBuilder : CollectionModelBuilder
}
/// <summary>
/// Builds and returns a <see cref="CollectionModel"/> from the given <paramref name="type"/> and <paramref name="definition"/>.
/// Builds and returns a <see cref="CollectionModel"/> from the given <paramref name="recordType"/> and <paramref name="definition"/>.
/// </summary>
[RequiresDynamicCode("This model building variant is not compatible with NativeAOT. See BuildDynamic() for dynamic mapping, and a third variant accepting source-generated delegates will be introduced in the future.")]
[RequiresUnreferencedCode("This model building variant is not compatible with trimming. See BuildDynamic() for dynamic mapping, and a third variant accepting source-generated delegates will be introduced in the future.")]
public virtual CollectionModel Build(
Type type,
Type recordType,
Type keyType,
VectorStoreCollectionDefinition? definition,
IEmbeddingGenerator? defaultEmbeddingGenerator,
JsonSerializerOptions jsonSerializerOptions)
{
this._jsonSerializerOptions = jsonSerializerOptions;
return this.Build(type, definition, defaultEmbeddingGenerator);
return this.Build(recordType, keyType, definition, defaultEmbeddingGenerator);
}
/// <summary>
@@ -53,6 +53,12 @@ public abstract class CollectionModelBuilder
/// </summary>
protected IEmbeddingGenerator? DefaultEmbeddingGenerator { get; private set; }
/// <summary>
/// Gets the collection's generic key type parameter (<c>TKey</c>), if provided.
/// Used by <see cref="ValidateKeyProperty"/> to validate that <c>TKey</c> corresponds to the key property type on the model.
/// </summary>
protected Type? KeyType { get; private set; }
/// <summary>
/// Constructs a new <see cref="CollectionModelBuilder"/>.
/// </summary>
@@ -60,32 +66,38 @@ public abstract class CollectionModelBuilder
=> this.Options = options;
/// <summary>
/// Builds and returns an <see cref="CollectionModel"/> from the given <paramref name="type"/> and <paramref name="definition"/>.
/// Builds and returns an <see cref="CollectionModel"/> from the given <paramref name="recordType"/> and <paramref name="definition"/>.
/// </summary>
/// <param name="recordType">The CLR type of the record.</param>
/// <param name="keyType">The collection's generic key type parameter (<c>TKey</c>), used to validate correspondence with the key property type.</param>
/// <param name="definition">An optional record definition that overrides attribute-based configuration.</param>
/// <param name="defaultEmbeddingGenerator">An optional default embedding generator for vector properties.</param>
[RequiresDynamicCode("This model building variant is not compatible with NativeAOT. See BuildDynamic() for dynamic mapping, and a third variant accepting source-generated delegates will be introduced in the future.")]
[RequiresUnreferencedCode("This model building variant is not compatible with trimming. See BuildDynamic() for dynamic mapping, and a third variant accepting source-generated delegates will be introduced in the future.")]
public virtual CollectionModel Build(Type type, VectorStoreCollectionDefinition? definition, IEmbeddingGenerator? defaultEmbeddingGenerator)
public virtual CollectionModel Build(Type recordType, Type keyType, VectorStoreCollectionDefinition? definition, IEmbeddingGenerator? defaultEmbeddingGenerator)
{
if (type == typeof(Dictionary<string, object?>))
this.KeyType = keyType;
if (recordType == typeof(Dictionary<string, object?>))
{
throw new ArgumentException("Dynamic mapping with Dictionary<string, object?> requires calling BuildDynamic().");
}
this.DefaultEmbeddingGenerator = definition?.EmbeddingGenerator ?? defaultEmbeddingGenerator;
this.ProcessTypeProperties(type, definition);
this.ProcessTypeProperties(recordType, definition);
if (definition is not null)
{
this.ProcessRecordDefinition(definition, type);
this.ProcessRecordDefinition(definition, recordType);
}
// Go over the properties, set the PropertyInfos to point to the .NET type's properties and validate type compatibility.
foreach (var property in this.Properties)
{
// When we have a CLR type (POCO, not dynamic mapping), get the .NET property's type and make sure it matches the definition.
property.PropertyInfo = type.GetProperty(property.ModelName)
?? throw new InvalidOperationException($"Property '{property.ModelName}' not found on CLR type '{type.FullName}'.");
property.PropertyInfo = recordType.GetProperty(property.ModelName)
?? throw new InvalidOperationException($"Property '{property.ModelName}' not found on CLR type '{recordType.FullName}'.");
var clrPropertyType = property.PropertyInfo.PropertyType;
if ((Nullable.GetUnderlyingType(clrPropertyType) ?? clrPropertyType) != (Nullable.GetUnderlyingType(property.Type) ?? property.Type))
@@ -96,20 +108,22 @@ public abstract class CollectionModelBuilder
}
this.Customize();
this.Validate(type, definition);
this.Validate(recordType, definition);
// Extra validation for non-dynamic mapping scenarios: ensure the type has a parameterless constructor.
if (!this.Options.UsesExternalSerializer && type.GetConstructor(Type.EmptyTypes) is null)
if (!this.Options.UsesExternalSerializer && recordType.GetConstructor(Type.EmptyTypes) is null)
{
throw new NotSupportedException($"Type '{type.Name}' must have a parameterless constructor.");
throw new NotSupportedException($"Type '{recordType.Name}' must have a parameterless constructor.");
}
return new(type, new ActivatorBasedRecordCreator(), this.KeyProperties, this.DataProperties, this.VectorProperties, this.PropertyMap);
return new(recordType, new ActivatorBasedRecordCreator(), this.KeyProperties, this.DataProperties, this.VectorProperties, this.PropertyMap);
}
/// <summary>
/// Builds and returns an <see cref="CollectionModel"/> for dynamic mapping scenarios from the given <paramref name="definition"/>.
/// </summary>
/// <param name="definition">The record definition describing the collection's schema.</param>
/// <param name="defaultEmbeddingGenerator">An optional default embedding generator for vector properties.</param>
public virtual CollectionModel BuildDynamic(VectorStoreCollectionDefinition definition, IEmbeddingGenerator? defaultEmbeddingGenerator)
{
if (definition is null)
@@ -531,10 +545,18 @@ public abstract class CollectionModelBuilder
=> keyPropertyType == typeof(Guid);
/// <summary>
/// Validates that the .NET type for a key property is supported by the provider.
/// Validates the key property. The default implementation validates that the collection's generic key type (<see cref="KeyType"/>)
/// corresponds to the key property type on the model, if <see cref="KeyType"/> was provided.
/// Provider overrides should call the base implementation.
/// </summary>
/// <returns><c>true</c> if the provider supports auto-generating keys of the specified type; otherwise, <c>false</c>.</returns>
protected abstract void ValidateKeyProperty(KeyPropertyModel keyProperty);
protected virtual void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
if (this.KeyType is not null && this.KeyType != typeof(object) && this.KeyType != keyProperty.Type)
{
throw new InvalidOperationException(
$"The collection's generic key type is '{this.KeyType.Name}', but the key property '{keyProperty.ModelName}' has type '{keyProperty.Type.Name}'. The generic key type must match the key property type.");
}
}
/// <summary>
/// Validates that the .NET type for a data property is supported by the provider.
@@ -84,7 +84,7 @@ public class WeaviateCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRe
static options => typeof(TRecord) == typeof(Dictionary<string, object?>)
? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(WeaviateDynamicCollection)))
: new WeaviateModelBuilder(options.HasNamedVectors)
.Build(typeof(TRecord), options.Definition, options.EmbeddingGenerator, WeaviateConstants.s_jsonSerializerOptions),
.Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator, WeaviateConstants.s_jsonSerializerOptions),
options)
{
}
@@ -24,6 +24,8 @@ internal class WeaviateModelBuilder(bool hasNamedVectors) : CollectionJsonModelB
protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
{
base.ValidateKeyProperty(keyProperty);
if (keyProperty.Type != typeof(Guid))
{
throw new NotSupportedException(
@@ -103,8 +103,7 @@ public sealed class CosmosMongoCollectionSearchMappingTests
private static CollectionModel BuildModel(List<VectorStoreProperty> properties)
=> new MongoModelBuilder()
.Build(
typeof(Dictionary<string, object?>),
.BuildDynamic(
new() { Properties = properties },
defaultEmbeddingGenerator: null);
}
@@ -32,7 +32,7 @@ public sealed class MongoMapperTests
]
};
this._sut = new(new MongoModelBuilder().Build(typeof(MongoHotelModel), definition, defaultEmbeddingGenerator: null));
this._sut = new(new MongoModelBuilder().Build(typeof(MongoHotelModel), typeof(string), definition, defaultEmbeddingGenerator: null));
}
[Fact]
@@ -119,6 +119,7 @@ public class QdrantCollectionSearchMappingTests
var model = new QdrantModelBuilder(hasNamedVectors: false)
.Build(
typeof(DataModel),
typeof(ulong),
new()
{
Properties =
@@ -24,7 +24,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateSinglePropsVectorStoreRecordDefinition(typeof(ulong));
var model = new QdrantModelBuilder(hasNamedVectors)
.Build(typeof(SinglePropsModel<ulong>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(SinglePropsModel<ulong>), typeof(ulong), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<SinglePropsModel<ulong>>(model, hasNamedVectors);
// Act.
@@ -54,7 +54,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateSinglePropsVectorStoreRecordDefinition(typeof(Guid));
var model = new QdrantModelBuilder(hasNamedVectors)
.Build(typeof(SinglePropsModel<Guid>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(SinglePropsModel<Guid>), typeof(Guid), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<SinglePropsModel<Guid>>(model, hasNamedVectors);
// Act.
@@ -77,7 +77,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateSinglePropsVectorStoreRecordDefinition(typeof(ulong));
var model = new QdrantModelBuilder(hasNamedVectors)
.Build(typeof(SinglePropsModel<ulong>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(SinglePropsModel<ulong>), typeof(ulong), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<SinglePropsModel<ulong>>(model, hasNamedVectors);
// Act.
@@ -109,7 +109,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateSinglePropsVectorStoreRecordDefinition(typeof(Guid));
var model = new QdrantModelBuilder(hasNamedVectors)
.Build(typeof(SinglePropsModel<Guid>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(SinglePropsModel<Guid>), typeof(Guid), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<SinglePropsModel<Guid>>(model, hasNamedVectors);
// Act.
@@ -137,7 +137,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateMultiPropsVectorStoreRecordDefinition(typeof(ulong));
var model = new QdrantModelBuilder(hasNamedVectors: true)
.Build(typeof(MultiPropsModel<ulong>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(MultiPropsModel<ulong>), typeof(ulong), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<MultiPropsModel<ulong>>(model, hasNamedVectors: true);
@@ -166,7 +166,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateMultiPropsVectorStoreRecordDefinition(typeof(Guid));
var model = new QdrantModelBuilder(hasNamedVectors: true)
.Build(typeof(MultiPropsModel<Guid>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(MultiPropsModel<Guid>), typeof(Guid), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<MultiPropsModel<Guid>>(model, hasNamedVectors: true);
// Act.
@@ -196,7 +196,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateMultiPropsVectorStoreRecordDefinition(typeof(ulong));
var model = new QdrantModelBuilder(hasNamedVectors: true)
.Build(typeof(MultiPropsModel<ulong>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(MultiPropsModel<ulong>), typeof(ulong), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<MultiPropsModel<ulong>>(model, hasNamedVectors: true);
// Act.
@@ -235,7 +235,7 @@ public class QdrantMapperTests
// Arrange.
var definition = CreateMultiPropsVectorStoreRecordDefinition(typeof(Guid));
var model = new QdrantModelBuilder(hasNamedVectors: true)
.Build(typeof(MultiPropsModel<Guid>), definition, defaultEmbeddingGenerator: null);
.Build(typeof(MultiPropsModel<Guid>), typeof(Guid), definition, defaultEmbeddingGenerator: null);
var sut = new QdrantMapper<MultiPropsModel<Guid>>(model, hasNamedVectors: true);
// Act.
@@ -16,7 +16,7 @@ public sealed class RedisHashSetMapperTests
{
private static readonly CollectionModel s_model
= new RedisModelBuilder(RedisHashSetCollection<string, AllTypesModel>.ModelBuildingOptions)
.Build(typeof(AllTypesModel), RedisHashSetMappingTestHelpers.s_definition, defaultEmbeddingGenerator: null);
.Build(typeof(AllTypesModel), typeof(string), RedisHashSetMappingTestHelpers.s_definition, defaultEmbeddingGenerator: null);
[Fact]
public void MapsAllFieldsFromDataToStorageModel()
@@ -20,7 +20,7 @@ public sealed class RedisJsonMapperTests
{
// Arrange.
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
.Build(typeof(MultiPropsModel), definition: null, defaultEmbeddingGenerator: null, JsonSerializerOptions.Default);
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, JsonSerializerOptions.Default);
var sut = new RedisJsonMapper<MultiPropsModel>(model, JsonSerializerOptions.Default);
// Act.
@@ -42,7 +42,7 @@ public sealed class RedisJsonMapperTests
// Arrange.
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
.Build(typeof(MultiPropsModel), definition: null, defaultEmbeddingGenerator: null, jsonSerializerOptions);
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, jsonSerializerOptions);
var sut = new RedisJsonMapper<MultiPropsModel>(model, jsonSerializerOptions);
// Act.
@@ -63,7 +63,7 @@ public sealed class RedisJsonMapperTests
{
// Arrange.
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
.Build(typeof(MultiPropsModel), definition: null, defaultEmbeddingGenerator: null, JsonSerializerOptions.Default);
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, JsonSerializerOptions.Default);
var sut = new RedisJsonMapper<MultiPropsModel>(model, JsonSerializerOptions.Default);
// Act.
@@ -91,7 +91,7 @@ public sealed class RedisJsonMapperTests
// Arrange.
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
.Build(typeof(MultiPropsModel), definition: null, defaultEmbeddingGenerator: null, jsonSerializerOptions);
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, jsonSerializerOptions);
var sut = new RedisJsonMapper<MultiPropsModel>(model, jsonSerializerOptions);
// Act.
@@ -18,6 +18,19 @@ public abstract class KeyTypeTests(KeyTypeTests.Fixture fixture)
new Guid("603840bf-cf91-4521-8b8e-8b6a2e75910a"),
supportsAutoGeneration: true);
/// <summary>
/// Verifies that creating a collection with a TKey that doesn't match the key property type on the model throws.
/// </summary>
[ConditionalFact]
public virtual void MismatchedKeyTypeThrows()
{
// The definition says the key property is string (matching Record<string>.Key),
// but TKey is Guid - this mismatch should be detected during model building.
Assert.Throws<InvalidOperationException>(() =>
fixture.TestStore.CreateCollection<Guid, Record<string>>(
fixture.CollectionName, fixture.CreateRecordDefinition<string>(withAutoGeneration: false)));
}
protected virtual Task Test<TKey>(TKey key, bool supportsAutoGeneration = false)
where TKey : struct
=> this.Test<TKey>(key, default!, supportsAutoGeneration: supportsAutoGeneration);
@@ -21,7 +21,7 @@ public class CollectionModelBuilderTests
public void Default_embedding_generator_without_record_definition()
{
using var embeddingGenerator = new FakeEmbeddingGenerator<string, Embedding<float>>();
var model = new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), definition: null, embeddingGenerator);
var model = new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), typeof(int), definition: null, embeddingGenerator);
// The embedding's .NET type (Embedding<float>) is inferred from the embedding generator.
Assert.Same(embeddingGenerator, model.VectorProperty.EmbeddingGenerator);
@@ -48,7 +48,7 @@ public class CollectionModelBuilderTests
]
};
var model = new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), recordDefinition, embeddingGenerator);
var model = new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), typeof(int), recordDefinition, embeddingGenerator);
// The embedding's .NET type (Embedding<float>) is inferred from the embedding generator.
Assert.Same(embeddingGenerator, model.VectorProperty.EmbeddingGenerator);
@@ -147,7 +147,7 @@ public class CollectionModelBuilderTests
]
},
embeddingGenerator)
: new CustomModelBuilder().Build(typeof(RecordWithEmbeddingVectorProperty), definition: null, embeddingGenerator);
: new CustomModelBuilder().Build(typeof(RecordWithEmbeddingVectorProperty), typeof(int), definition: null, embeddingGenerator);
var vectorProperty = model.VectorProperty;
Assert.Same(embeddingGenerator, vectorProperty.EmbeddingGenerator);
@@ -161,6 +161,7 @@ public class CollectionModelBuilderTests
var model = new CustomModelBuilder().Build(
typeof(RecordWithEmbeddingVectorProperty),
typeof(int),
new VectorStoreCollectionDefinition
{
Properties =
@@ -200,7 +201,7 @@ public class CollectionModelBuilderTests
var model = dynamic
? new CustomModelBuilder().BuildDynamic(recordDefinition, embeddingGenerator)
: new CustomModelBuilder().Build(typeof(RecordWithCustomerVectorProperty), recordDefinition, embeddingGenerator);
: new CustomModelBuilder().Build(typeof(RecordWithCustomerVectorProperty), typeof(int), recordDefinition, embeddingGenerator);
var vectorProperty = model.VectorProperty;
@@ -216,7 +217,7 @@ public class CollectionModelBuilderTests
using var embeddingGenerator = new FakeEmbeddingGenerator<string, Embedding<long>>();
var exception = Assert.Throws<InvalidOperationException>(() =>
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), definition: null, embeddingGenerator));
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), typeof(int), definition: null, embeddingGenerator));
Assert.Equal($"Embedding generator 'FakeEmbeddingGenerator<string, Embedding<long>>' on vector property '{nameof(RecordWithStringVectorProperty.Embedding)}' cannot convert the input type 'string' to a supported vector type (one of: ReadOnlyMemory<float>, Embedding<float>, float[], ReadOnlyMemory<Half>, Embedding<Half>, Half[]).", exception.Message);
}
@@ -228,7 +229,7 @@ public class CollectionModelBuilderTests
using var embeddingGenerator = new FakeEmbeddingGenerator<int, Embedding<float>>();
var exception = Assert.Throws<InvalidOperationException>(() =>
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), definition: null, embeddingGenerator));
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), typeof(int), definition: null, embeddingGenerator));
Assert.Equal($"Embedding generator 'FakeEmbeddingGenerator<int, Embedding<float>>' on vector property '{nameof(RecordWithStringVectorProperty.Embedding)}' cannot convert the input type 'string' to a supported vector type (one of: ReadOnlyMemory<float>, Embedding<float>, float[], ReadOnlyMemory<Half>, Embedding<Half>, Half[]).", exception.Message);
}
@@ -237,7 +238,7 @@ public class CollectionModelBuilderTests
public void Non_embedding_vector_property_without_embedding_generator_throws()
{
var exception = Assert.Throws<InvalidOperationException>(() =>
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), definition: null, defaultEmbeddingGenerator: null));
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), typeof(int), definition: null, defaultEmbeddingGenerator: null));
Assert.Equal($"Vector property '{nameof(RecordWithStringVectorProperty.Embedding)}' has type 'string' which isn't supported by your provider, and no embedding generator is configured. Configure a generator that supports converting 'string' to vector type supported by your provider.", exception.Message);
}
@@ -261,7 +262,7 @@ public class CollectionModelBuilderTests
};
var exception = Assert.Throws<InvalidOperationException>(() =>
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), recordDefinition, embeddingGenerator));
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), typeof(int), recordDefinition, embeddingGenerator));
Assert.Equal("Vector property 'Embedding' has embedding type 'Embedding<Byte>' configured, but that type isn't supported by your provider. Supported types are ReadOnlyMemory<float>, Embedding<float>, float[], ReadOnlyMemory<Half>, Embedding<Half>, Half[].", exception.Message);
}
@@ -285,7 +286,7 @@ public class CollectionModelBuilderTests
};
var exception = Assert.Throws<InvalidOperationException>(() =>
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), recordDefinition, embeddingGenerator));
new CustomModelBuilder().Build(typeof(RecordWithStringVectorProperty), typeof(int), recordDefinition, embeddingGenerator));
Assert.Equal("Vector property 'Embedding' has embedding type 'Embedding<Half>' configured, but that type isn't supported by your embedding generator.", exception.Message);
}
@@ -111,6 +111,7 @@ public sealed class WeaviateMapperTests
new WeaviateModelBuilder(hasNamedVectors)
.Build(
typeof(WeaviateHotel),
typeof(Guid),
new VectorStoreCollectionDefinition
{
Properties =