File: ProviderServices\CollectionModelBuilder.cs
Project: ..\..\..\src\Libraries\Microsoft.Extensions.VectorData.Abstractions\Microsoft.Extensions.VectorData.Abstractions.csproj (Microsoft.Extensions.VectorData.Abstractions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
 
namespace Microsoft.Extensions.VectorData.ProviderServices;
 
/// <summary>
/// Represents a builder for a <see cref="CollectionModel"/>.
/// This is an internal support type meant for use by providers only and not by applications.
/// </summary>
/// <remarks>This class is single-use only, and not thread-safe.</remarks>
[Experimental(DiagnosticIds.Experiments.VectorDataProviderServices, UrlFormat = DiagnosticIds.UrlFormat)]
public abstract class CollectionModelBuilder
{
    /// <summary>
    /// Gets the options for building the model.
    /// </summary>
    protected CollectionModelBuildingOptions Options { get; }
 
#pragma warning disable CA1002 // Do not expose generic lists
    /// <summary>
    /// Gets the key properties of the record.
    /// </summary>
    protected List<KeyPropertyModel> KeyProperties { get; } = [];
 
    /// <summary>
    /// Gets the data properties of the record.
    /// </summary>
    protected List<DataPropertyModel> DataProperties { get; } = [];
 
    /// <summary>
    /// Gets the vector properties of the record.
    /// </summary>
    protected List<VectorPropertyModel> VectorProperties { get; } = [];
#pragma warning restore CA1002 // Do not expose generic lists
 
    /// <summary>
    /// Gets all properties of the record, of all types.
    /// </summary>
    protected IEnumerable<PropertyModel> Properties => PropertyMap.Values;
 
    /// <summary>
    /// Gets all properties of the record, of all types, indexed by their model name.
    /// </summary>
    protected Dictionary<string, PropertyModel> PropertyMap { get; } = [];
 
    /// <summary>
    /// Gets the default embedding generator to use for vector properties, when none is specified at the property or collection level.
    /// </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>
    /// Initializes a new instance of the <see cref="CollectionModelBuilder"/> class.
    /// </summary>
    protected CollectionModelBuilder(CollectionModelBuildingOptions options)
    {
        Options = options;
    }
 
    /// <summary>
    /// 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>
    /// <returns>The built <see cref="CollectionModel"/>.</returns>
    [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 recordType, Type keyType, VectorStoreCollectionDefinition? definition, IEmbeddingGenerator? defaultEmbeddingGenerator)
    {
        _ = Throw.IfNull(recordType);
 
        KeyType = keyType;
 
        if (recordType == typeof(Dictionary<string, object?>))
        {
            Throw.ArgumentException(nameof(recordType), "Dynamic mapping with Dictionary<string, object?> requires calling BuildDynamic().");
        }
 
        DefaultEmbeddingGenerator = definition?.EmbeddingGenerator ?? defaultEmbeddingGenerator;
 
        // Build a lookup of definition properties by name for matching with CLR properties.
        Dictionary<string, VectorStoreProperty>? definitionByName = null;
        if (definition is not null)
        {
            definitionByName = [];
            foreach (var p in definition.Properties)
            {
                definitionByName[p.Name] = p;
            }
        }
 
        // Process CLR properties, matching to definition properties where available.
        // TODO: This traverses the CLR type's properties, making it incompatible with trimming (and NativeAOT).
        // TODO: We could put [DynamicallyAccessedMembers] to preserve all properties, but that approach wouldn't
        // TODO: work with hierarchical data models (https://github.com/microsoft/semantic-kernel/issues/10957).
        foreach (var clrProperty in recordType.GetProperties())
        {
            VectorStoreProperty? definitionProperty = null;
            _ = definitionByName?.TryGetValue(clrProperty.Name, out definitionProperty);
 
            ProcessProperty(clrProperty, definitionProperty);
        }
 
        // Go over the properties, configure POCO accessors and validate type compatibility.
        foreach (var property in Properties)
        {
            var clrProperty = recordType.GetProperty(property.ModelName)
                ?? throw new InvalidOperationException($"Property '{property.ModelName}' not found on CLR type '{recordType.FullName}'.");
 
            var clrPropertyType = clrProperty.PropertyType;
            if ((Nullable.GetUnderlyingType(clrPropertyType) ?? clrPropertyType) != (Nullable.GetUnderlyingType(property.Type) ?? property.Type))
            {
                throw new InvalidOperationException(
                    $"Property '{property.ModelName}' has a different CLR type in the record definition ('{property.Type.Name}') and on the .NET property ('{clrProperty.PropertyType}').");
            }
 
            property.ConfigurePocoAccessors(clrProperty);
        }
 
        Customize();
        Validate(recordType, definition);
 
        // Extra validation for non-dynamic mapping scenarios: ensure the type has a parameterless constructor.
        if (!Options.UsesExternalSerializer && recordType.GetConstructor(Type.EmptyTypes) is null)
        {
            throw new NotSupportedException($"Type '{recordType.Name}' must have a parameterless constructor.");
        }
 
        return new(recordType, () => Activator.CreateInstance(recordType)!, KeyProperties, DataProperties, VectorProperties, 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>
    /// <returns>The built <see cref="CollectionModel"/>.</returns>
    public virtual CollectionModel BuildDynamic(VectorStoreCollectionDefinition definition, IEmbeddingGenerator? defaultEmbeddingGenerator)
    {
        _ = Throw.IfNull(definition);
 
        DefaultEmbeddingGenerator = defaultEmbeddingGenerator;
 
        foreach (var defProp in definition.Properties)
        {
            ProcessProperty(clrProperty: null, defProp);
        }
 
        Customize();
        Validate(type: null, definition);
 
        foreach (var property in Properties)
        {
            property.ConfigureDynamicAccessors();
        }
 
        return new(typeof(Dictionary<string, object?>), static () => new Dictionary<string, object?>(), KeyProperties, DataProperties, VectorProperties, PropertyMap);
    }
 
    /// <summary>
    /// As part of building the model, this method processes a single property, accepting both a CLR <see cref="PropertyInfo"/>
    /// (from which attributes are read) and a <see cref="VectorStoreProperty"/> from the user-provided record definition.
    /// Either may be <see langword="null"/>, but not both.
    /// When both are provided, the record definition values override attribute-configured values.
    /// </summary>
    protected virtual void ProcessProperty(PropertyInfo? clrProperty, VectorStoreProperty? definitionProperty)
    {
        Debug.Assert(clrProperty is not null || definitionProperty is not null, "Either a CLR property or a definition property must be provided.");
 
        VectorStoreKeyAttribute? keyAttribute = null;
        VectorStoreDataAttribute? dataAttribute = null;
        VectorStoreVectorAttribute? vectorAttribute = null;
 
        if (clrProperty is not null)
        {
            // Read attributes from CLR property.
            keyAttribute = clrProperty.GetCustomAttribute<VectorStoreKeyAttribute>();
            dataAttribute = clrProperty.GetCustomAttribute<VectorStoreDataAttribute>();
            vectorAttribute = clrProperty.GetCustomAttribute<VectorStoreVectorAttribute>();
 
            // Validate that at most one mapping attribute is present.
            if ((keyAttribute is not null ? 1 : 0) + (dataAttribute is not null ? 1 : 0) + (vectorAttribute is not null ? 1 : 0) > 1)
            {
                throw new InvalidOperationException(
                    $"Property '{clrProperty.DeclaringType?.Name}.{clrProperty.Name}' has multiple of {nameof(VectorStoreKeyAttribute)}, {nameof(VectorStoreDataAttribute)} or {nameof(VectorStoreVectorAttribute)}. Only one of these attributes can be specified on a property.");
            }
 
            // If no mapping attribute and no definition, skip this property.
            if (keyAttribute is null && dataAttribute is null && vectorAttribute is null && definitionProperty is null)
            {
                return;
            }
 
            // Validate kind compatibility between attribute and definition.
            if (definitionProperty is not null
                && ((keyAttribute is not null && definitionProperty is not VectorStoreKeyProperty)
                    || (dataAttribute is not null && definitionProperty is not VectorStoreDataProperty)
                    || (vectorAttribute is not null && definitionProperty is not VectorStoreVectorProperty)))
            {
                string definitionKind = definitionProperty switch
                {
                    VectorStoreKeyProperty => "key",
                    VectorStoreDataProperty => "data",
                    VectorStoreVectorProperty => "vector",
                    _ => throw new ArgumentException($"Unknown type '{definitionProperty.GetType().FullName}' in vector store record definition.")
                };
 
                throw new InvalidOperationException(
                    $"Property '{clrProperty.Name}' is present in the {nameof(VectorStoreCollectionDefinition)} as a {definitionKind} property, but the .NET property on type '{clrProperty.DeclaringType?.Name}' has an incompatible attribute.");
            }
        }
 
        string propertyName = clrProperty?.Name ?? definitionProperty?.Name ?? throw new UnreachableException();
        Type propertyType = clrProperty?.PropertyType
            ?? definitionProperty?.Type
            ?? throw new InvalidOperationException(VectorDataStrings.MissingTypeOnPropertyDefinition(definitionProperty!));
 
        PropertyModel property;
        string? attributeStorageName = null;
 
        if (keyAttribute is not null || definitionProperty is VectorStoreKeyProperty)
        {
            var keyProperty = new KeyPropertyModel(propertyName, propertyType);
 
            if (keyAttribute is not null)
            {
                keyProperty.IsAutoGenerated = keyAttribute.IsAutoGeneratedNullable ?? SupportsKeyAutoGeneration(keyProperty.Type);
                attributeStorageName = keyAttribute.StorageName;
            }
 
            // Definition values override attribute values.
            if (definitionProperty is VectorStoreKeyProperty defKey)
            {
                keyProperty.IsAutoGenerated = defKey.IsAutoGenerated ?? SupportsKeyAutoGeneration(keyProperty.Type);
            }
 
            KeyProperties.Add(keyProperty);
            property = keyProperty;
        }
        else if (dataAttribute is not null || definitionProperty is VectorStoreDataProperty)
        {
            var dataProperty = new DataPropertyModel(propertyName, propertyType);
 
            if (dataAttribute is not null)
            {
                dataProperty.IsIndexed = dataAttribute.IsIndexed;
                dataProperty.IsFullTextIndexed = dataAttribute.IsFullTextIndexed;
                attributeStorageName = dataAttribute.StorageName;
            }
 
            // Definition values override attribute values.
            if (definitionProperty is VectorStoreDataProperty defData)
            {
                dataProperty.IsIndexed = defData.IsIndexed;
                dataProperty.IsFullTextIndexed = defData.IsFullTextIndexed;
            }
 
            DataProperties.Add(dataProperty);
            property = dataProperty;
        }
        else if (vectorAttribute is not null || definitionProperty is VectorStoreVectorProperty)
        {
            // If a definition exists, create via the definition to preserve generic type info (VectorStoreVectorProperty<TInput>).
            var vectorProperty = definitionProperty is VectorStoreVectorProperty defVec
                ? defVec.CreatePropertyModel()
                : new VectorPropertyModel(propertyName, propertyType);
 
            if (vectorAttribute is not null)
            {
                vectorProperty.Dimensions = vectorAttribute.Dimensions;
                vectorProperty.IndexKind = vectorAttribute.IndexKind;
                vectorProperty.DistanceFunction = vectorAttribute.DistanceFunction;
                attributeStorageName = vectorAttribute.StorageName;
            }
 
            // Definition values override attribute values.
            if (definitionProperty is VectorStoreVectorProperty defVectorProp)
            {
                vectorProperty.Dimensions = defVectorProp.Dimensions;
 
                if (defVectorProp.IndexKind is not null)
                {
                    vectorProperty.IndexKind = defVectorProp.IndexKind;
                }
 
                if (defVectorProp.DistanceFunction is not null)
                {
                    vectorProperty.DistanceFunction = defVectorProp.DistanceFunction;
                }
            }
 
            ConfigureVectorPropertyEmbedding(
                vectorProperty,
                (definitionProperty as VectorStoreVectorProperty)?.EmbeddingGenerator ?? DefaultEmbeddingGenerator,
                (definitionProperty as VectorStoreVectorProperty)?.EmbeddingType);
 
            VectorProperties.Add(vectorProperty);
            property = vectorProperty;
        }
        else
        {
            throw new UnreachableException();
        }
 
        if (clrProperty is not null)
        {
            property.PropertyInfo = clrProperty;
        }
 
        // Apply storage name: attribute first, then definition (which takes precedence).
        SetPropertyStorageName(property, attributeStorageName);
        if (definitionProperty is not null)
        {
            SetPropertyStorageName(property, definitionProperty.StorageName);
        }
 
        if (definitionProperty?.ProviderAnnotations is not null)
        {
            property.ProviderAnnotations = new Dictionary<string, object?>(definitionProperty.ProviderAnnotations);
        }
 
        PropertyMap.Add(propertyName, property);
    }
 
    private void SetPropertyStorageName(PropertyModel property, string? storageName)
    {
        if (property is KeyPropertyModel && Options.ReservedKeyStorageName is not null)
        {
            // If we have ReservedKeyStorageName, there can only be a single key property (validated in the constructor)
            property.StorageName = Options.ReservedKeyStorageName;
            return;
        }
 
        if (storageName is null)
        {
            return;
        }
 
        // If a custom serializer is used (e.g. JsonSerializer), it would ignore our own attributes/config, and
        // our model needs to be in sync with the serializer's behavior (for e.g. storage names in filters).
        // So we ignore the config here as well.
        // TODO: Consider throwing here instead of ignoring
        if (Options.UsesExternalSerializer && property.PropertyInfo is not null)
        {
            return;
        }
 
        property.StorageName = storageName;
    }
 
    /// <summary>
    /// Gets the embedding types supported by this provider, in priority order.
    /// The first type whose embedding generator is compatible with the input type will be used.
    /// </summary>
    /// <remarks>
    /// Override this property in providers that support additional embedding types beyond <see cref="Embedding{T}"/> of <see langword="float"/>.
    /// </remarks>
    protected virtual IReadOnlyList<EmbeddingGenerationDispatcher> EmbeddingGenerationDispatchers { get; }
        = [EmbeddingGenerationDispatcher.Create<Embedding<float>>()];
 
    /// <summary>
    /// Attempts to resolve the embedding type for the given vector property, iterating over <see cref="EmbeddingGenerationDispatchers"/> in priority order.
    /// </summary>
    private (Type? EmbeddingType, EmbeddingGenerationDispatcher? Handler) ResolveEmbeddingType(
        VectorPropertyModel vectorProperty,
        IEmbeddingGenerator embeddingGenerator,
        Type? userRequestedEmbeddingType)
    {
        foreach (var supported in EmbeddingGenerationDispatchers)
        {
            if (supported.ResolveEmbeddingType(vectorProperty, embeddingGenerator, userRequestedEmbeddingType) is { } resolved)
            {
                return (resolved, supported);
            }
        }
 
        return (null, null);
    }
 
    /// <summary>
    /// Resolves the embedding handler for a native vector property type, where embedding generation is only needed for search.
    /// Since the property type is already a valid native type, we only check if the generator can produce the
    /// embedding output type (regardless of input type, which is only known at search time).
    /// </summary>
    private EmbeddingGenerationDispatcher? ResolveSearchOnlyEmbeddingHandler(VectorPropertyModel vectorProperty, IEmbeddingGenerator embeddingGenerator)
    {
        foreach (var supported in EmbeddingGenerationDispatchers)
        {
            if (supported.CanGenerateEmbedding(vectorProperty, embeddingGenerator))
            {
                return supported;
            }
        }
 
        return null;
    }
 
    /// <summary>
    /// Configures embedding generation for a vector property. Sets the embedding generator, resolves the embedding type,
    /// and assigns the appropriate <see cref="EmbeddingGenerationDispatcher"/>.
    /// </summary>
    /// <remarks>
    /// If the property's type is natively supported (e.g. <see cref="ReadOnlyMemory{T}"/> of <see langword="float"/>), the embedding type
    /// is set to the property's type; if a generator is also configured, a search-only dispatcher is resolved so that search can convert
    /// arbitrary inputs (e.g. string) to embeddings.
    /// Otherwise, if a generator is configured, the embedding type is resolved from it. If resolution fails, the embedding type remains
    /// <see langword="null"/> and an error is deferred to the validation phase.
    /// </remarks>
    private void ConfigureVectorPropertyEmbedding(
        VectorPropertyModel vectorProperty,
        IEmbeddingGenerator? embeddingGenerator,
        Type? userRequestedEmbeddingType)
    {
        vectorProperty.EmbeddingGenerator = embeddingGenerator;
 
        if (IsVectorPropertyTypeValid(vectorProperty.Type, out _))
        {
            if (userRequestedEmbeddingType is not null && userRequestedEmbeddingType != vectorProperty.Type)
            {
                throw new InvalidOperationException(VectorDataStrings.DifferentEmbeddingTypeSpecifiedForNativelySupportedType(vectorProperty, userRequestedEmbeddingType));
            }
 
            vectorProperty.EmbeddingType = vectorProperty.Type;
 
            // Even for native types, if an embedding generator is configured, resolve the dispatcher
            // so that search can convert arbitrary inputs (e.g. string) to embeddings.
            if (embeddingGenerator is not null)
            {
                vectorProperty.EmbeddingGenerationDispatcher = ResolveSearchOnlyEmbeddingHandler(vectorProperty, embeddingGenerator);
            }
        }
        else if (embeddingGenerator is not null)
        {
            // The property type isn't a valid embedding type, but an embedding generator is configured.
            // Try to resolve the embedding type from it: if the configured generator supports translating the input type (e.g. string) to
            // an output type supported by the provider, we set that as the embedding type.
            // If this fails, EmbeddingType remains null and we defer the error to the validation phase.
            var (embeddingType, handler) = ResolveEmbeddingType(vectorProperty, embeddingGenerator, userRequestedEmbeddingType);
            vectorProperty.EmbeddingType = embeddingType;
            vectorProperty.EmbeddingGenerationDispatcher = handler;
        }
 
        // If the property type isn't valid and there's no embedding generator, that's an error.
        // But we throw later, in validation, to allow for provider customization to correct this invalid state after this step.
    }
 
    /// <summary>
    /// Extension hook for providers to be able to customize the model.
    /// </summary>
    protected virtual void Customize()
    {
    }
 
    /// <summary>
    /// Validates the model after all properties have been processed.
    /// </summary>
    protected virtual void Validate(Type? type, VectorStoreCollectionDefinition? definition)
    {
        if (KeyProperties.Count > 1)
        {
            throw new NotSupportedException($"Multiple key properties found on {TypeMessage()}the provided {nameof(VectorStoreCollectionDefinition)} while only one is supported.");
        }
 
        if (KeyProperties.Count == 0)
        {
            throw new NotSupportedException($"No key property found on {TypeMessage()}the provided {nameof(VectorStoreCollectionDefinition)} while at least one is required.");
        }
 
        if (Options.RequiresAtLeastOneVector && VectorProperties.Count == 0)
        {
            throw new NotSupportedException($"No vector property found on {TypeMessage()}the provided {nameof(VectorStoreCollectionDefinition)} while at least one is required.");
        }
 
        if (!Options.SupportsMultipleVectors && VectorProperties.Count > 1)
        {
            throw new NotSupportedException($"Multiple vector properties found on {TypeMessage()}the provided {nameof(VectorStoreCollectionDefinition)} while only one is supported.");
        }
 
        var storageNameMap = new Dictionary<string, PropertyModel>();
 
        foreach (var property in PropertyMap.Values)
        {
            ValidateProperty(property, definition);
 
            if (storageNameMap.TryGetValue(property.StorageName, out var otherproperty))
            {
                throw new InvalidOperationException($"Property '{property.ModelName}' is being mapped to storage name '{property.StorageName}', but property '{otherproperty.ModelName}' is already mapped to the same storage name.");
            }
 
            storageNameMap[property.StorageName] = property;
        }
 
        string TypeMessage() => type is null ? string.Empty : $"type '{type.Name}' or ";
    }
 
    /// <summary>
    /// Validates a single property, performing validation on it.
    /// </summary>
    protected virtual void ValidateProperty(PropertyModel propertyModel, VectorStoreCollectionDefinition? definition)
    {
        _ = Throw.IfNull(propertyModel);
 
        var type = propertyModel.Type;
 
        Debug.Assert(propertyModel.Type is not null, "propertyModel.Type must be non-null");
 
        switch (propertyModel)
        {
            case KeyPropertyModel keyProperty:
                if (keyProperty.IsAutoGenerated && !SupportsKeyAutoGeneration(keyProperty.Type))
                {
                    throw new NotSupportedException(
                        $"Property '{keyProperty.ModelName}' is configured for auto-generation, but key properties of type '{keyProperty.Type.Name}' do not support auto-generation.");
                }
 
                ValidateKeyProperty(keyProperty);
                break;
 
            case DataPropertyModel dataProperty:
                if (!IsDataPropertyTypeValid(dataProperty.Type, out var supportedTypes))
                {
                    throw new NotSupportedException(
                        $"Property '{dataProperty.ModelName}' has unsupported type '{type.Name}'. Data properties must be one of the supported types: {supportedTypes}.");
                }
                break;
 
            case VectorPropertyModel vectorProperty:
                if (vectorProperty.EmbeddingType is null)
                {
                    if (IsVectorPropertyTypeValid(vectorProperty.Type, out string? supportedVectorTypes))
                    {
                        throw new UnreachableException("EmbeddingType cannot be null when the property type is supported.");
                    }
 
                    if (vectorProperty.EmbeddingGenerator is null)
                    {
                        throw new InvalidOperationException(VectorDataStrings.UnsupportedVectorPropertyWithoutEmbeddingGenerator(vectorProperty));
                    }
 
                    // If the user has configured a desired embedding type (done to use en embedding type other than the provider's default one), throw errors tailored to that.
                    // Throw errors related to that.
                    var userRequestedEmbeddingType = definition?.Properties.OfType<VectorStoreVectorProperty>().SingleOrDefault(p => p.Name == vectorProperty.ModelName)?.EmbeddingType;
                    if (userRequestedEmbeddingType is not null)
                    {
                        throw new InvalidOperationException(IsVectorPropertyTypeValid(userRequestedEmbeddingType, out _)
                            ? VectorDataStrings.ConfiguredEmbeddingTypeIsUnsupportedByTheGenerator(vectorProperty, userRequestedEmbeddingType)
                            : VectorDataStrings.ConfiguredEmbeddingTypeIsUnsupportedByTheProvider(vectorProperty, userRequestedEmbeddingType, supportedVectorTypes));
                    }
 
                    throw new InvalidOperationException(VectorDataStrings.IncompatibleEmbeddingGenerator(vectorProperty, vectorProperty.EmbeddingGenerator, supportedVectorTypes));
                }
 
                if (!IsVectorPropertyTypeValid(vectorProperty.EmbeddingType, out string? supportedVectorTypes2))
                {
                    // Should in principle never happen, only with incorrect provider customization.
                    throw new InvalidOperationException($"Property '{vectorProperty.ModelName}' has unsupported embedding type '{vectorProperty.EmbeddingType.Name}'. Vector properties must be one of the supported types: {supportedVectorTypes2}.");
                }
 
                if (vectorProperty.Dimensions <= 0)
                {
                    throw new InvalidOperationException($"Vector property '{propertyModel.ModelName}' must have a positive number of dimensions.");
                }
 
                break;
 
            default:
                throw new UnreachableException();
        }
    }
 
    /// <summary>
    /// Configures auto-generation for the given key property.
    /// Defaults to configuring <see cref="Guid" /> key properties as auto-generated, and throwing if auto-generation is requested for
    /// any other type.
    /// </summary>
    /// <returns><see langword="true"/> if auto-generation is supported for the given key property type; otherwise, <see langword="false"/>.</returns>
    protected virtual bool SupportsKeyAutoGeneration(Type keyPropertyType)
        => keyPropertyType == typeof(Guid);
 
    /// <summary>
    /// 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>
    protected virtual void ValidateKeyProperty(KeyPropertyModel keyProperty)
    {
        _ = Throw.IfNull(keyProperty);
 
        if (KeyType is not null && KeyType != typeof(object) && KeyType != keyProperty.Type)
        {
            throw new InvalidOperationException(
                $"The collection's generic key type is '{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.
    /// </summary>
    /// <returns><see langword="true"/> if the type is valid; otherwise, <see langword="false"/>.</returns>
    protected abstract bool IsDataPropertyTypeValid(Type type, [NotNullWhen(false)] out string? supportedTypes);
 
    /// <summary>
    /// Validates that the .NET type for a vector property is supported by the provider.
    /// </summary>
    /// <returns><see langword="true"/> if the type is valid; otherwise, <see langword="false"/>.</returns>
    protected abstract bool IsVectorPropertyTypeValid(Type type, [NotNullWhen(false)] out string? supportedTypes);
}