File: ProviderServices\PropertyModel.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.Reflection;
using Microsoft.Shared.DiagnosticIds;
 
namespace Microsoft.Extensions.VectorData.ProviderServices;
 
/// <summary>
/// Represents a property on a vector store record.
/// This is an internal support type meant for use by providers only and not by applications.
/// </summary>
[Experimental(DiagnosticIds.Experiments.VectorDataProviderServices, UrlFormat = DiagnosticIds.UrlFormat)]
public abstract class PropertyModel(string modelName, Type type)
{
    private Func<object, object?>? _getter;
    private Action<object, object?>? _setter;
 
    /// <summary>
    /// Gets or sets the model name of the property.
    /// </summary>
    /// <remarks>
    /// If the property corresponds to a .NET property, this name is the name of that property.
    /// </remarks>
    public string ModelName { get; set; } = modelName;
 
    /// <summary>
    /// Gets or sets the storage name of the property.
    /// </summary>
    /// <remarks>
    /// This is the name to which the property is mapped in the vector store.
    /// </remarks>
    public string StorageName
    {
        get => field ?? ModelName;
        set;
    }
 
    /// <summary>
    /// Gets or sets the CLR type of the property.
    /// </summary>
    public Type Type { get; set; } = type;
 
    /// <summary>
    /// Gets or sets the reflection <see cref="PropertyInfo"/> for the .NET property.
    /// </summary>
    /// <value>
    /// The reflection <see cref="PropertyInfo"/> for the .NET property.
    /// <see langword="null"/> when using dynamic mapping.
    /// </value>
    public PropertyInfo? PropertyInfo { get; set; }
 
    /// <summary>
    /// Gets or sets a dictionary of provider-specific annotations for this property.
    /// </summary>
    /// <remarks>
    /// This allows setting database-specific configuration options that aren't universal across all vector stores.
    /// </remarks>
    public Dictionary<string, object?>? ProviderAnnotations { get; set; }
 
    /// <summary>
    /// Gets a value indicating whether the property type is nullable.
    /// </summary>
    /// <remarks>
    /// For value types, this is <see langword="true"/> when the type is <see cref="Nullable{T}"/>.
    /// For reference types on .NET 6+, this uses NRT annotations via <c>NullabilityInfoContext</c>
    /// when a <see cref="PropertyInfo"/> is available (that is, during POCO mapping);
    /// otherwise, reference types are assumed nullable.
    /// </remarks>
    public bool IsNullable
    {
        get
        {
            // Value types: nullable only if Nullable<T>
            if (Type.IsValueType)
            {
                return Nullable.GetUnderlyingType(Type) is not null;
            }
 
            // Reference types: check NRT annotation via NullabilityInfoContext when available
#if NET
            if (PropertyInfo is { } propertyInfo)
            {
                var nullabilityInfo = new NullabilityInfoContext().Create(propertyInfo);
                return nullabilityInfo.ReadState != NullabilityState.NotNull;
            }
#endif
 
            // Dynamic mapping or old framework: assume nullable for reference types
            return true;
        }
    }
 
    /// <summary>
    /// Configures the property accessors using a CLR <see cref="System.Reflection.PropertyInfo"/> for POCO mapping.
    /// </summary>
    // TODO: Implement compiled delegates for better performance, https://github.com/microsoft/semantic-kernel/issues/11122
    // TODO: Implement source-generated accessors for NativeAOT, https://github.com/microsoft/semantic-kernel/issues/10256
    internal void ConfigurePocoAccessors(PropertyInfo propertyInfo)
    {
        PropertyInfo = propertyInfo;
        _getter = propertyInfo.GetValue;
        _setter = (record, value) =>
        {
            // If the value is null, no need to set the property (it's the CLR default)
            if (value is not null)
            {
                propertyInfo.SetValue(record, value);
            }
        };
    }
 
    /// <summary>
    /// Configures the property accessors for dynamic mapping using <see cref="Dictionary{TKey, TValue}"/>.
    /// </summary>
    internal void ConfigureDynamicAccessors()
    {
        var propertyType = Type;
 
        _getter = record =>
        {
            var dictionary = (Dictionary<string, object?>)record;
            _ = dictionary.TryGetValue(ModelName, out var value);
 
            if (value is not null && value.GetType() != (Nullable.GetUnderlyingType(propertyType) ?? propertyType))
            {
                throw new InvalidCastException($"Property '{ModelName}' has a value of type '{value.GetType().Name}', but its configured type is '{propertyType.Name}'.");
            }
 
            return value;
        };
 
        _setter = (record, value) => ((Dictionary<string, object?>)record)[ModelName] = value;
    }
 
    /// <summary>
    /// Reads the property from the given <paramref name="record"/>, returning the value as an <see cref="object"/>.
    /// </summary>
    /// <returns>The property value.</returns>
    public object? GetValueAsObject(object record)
    {
        Debug.Assert(_getter is not null, "Property accessors have not been configured.");
        return _getter!(record);
    }
 
    /// <summary>
    /// Writes the property from the given <paramref name="record"/>, accepting the value to write as an <see cref="object"/>.
    /// </summary>
    public void SetValueAsObject(object record, object? value)
    {
        Debug.Assert(_setter is not null, "Property accessors have not been configured.");
        _setter!(record, value);
    }
 
    /// <summary>
    /// Reads the property from the given <paramref name="record"/>.
    /// </summary>
    /// <typeparam name="T">The type of the property value.</typeparam>
    /// <returns>The property value.</returns>
    // TODO: actually implement the generic accessors to avoid boxing, and make use of them in providers
    public T GetValue<T>(object record)
        => (T)GetValueAsObject(record)!;
 
    /// <summary>
    /// Writes the property from the given <paramref name="record"/>.
    /// </summary>
    /// <typeparam name="T">The type of the property value.</typeparam>
    // TODO: actually implement the generic accessors to avoid boxing, and make use of them in providers
    public void SetValue<T>(object record, T value)
        => SetValueAsObject(record, value);
}