Elastic.SemanticKernel.Connectors.Elasticsearch/Internal/Helpers/VectorStoreRecordMapping.cs (121 lines of code) (raw):

// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; namespace Microsoft.SemanticKernel.Data; #pragma warning disable IDE0004, IDE0059 /// <summary> /// Contains helper methods to map between storage and data models. /// </summary> [ExcludeFromCodeCoverage] internal static class VectorStoreRecordMapping { /// <summary> /// Loop through the list of <see cref="PropertyInfo"/> objects and for each one look up the storage name /// in the <paramref name="dataModelPropertiesInfo"/> and check if the value exists in the <paramref name="storageValues"/>. /// If so, set the value on the record object. /// </summary> /// <typeparam name="TStorageType">The type of the storage properties.</typeparam> /// <typeparam name="TRecord">The type of the target object.</typeparam> /// <param name="record">The target object to set the property values on.</param> /// <param name="dataModelPropertiesInfo"><see cref="PropertyInfo"/> objects listing the properties on the data model to get values for.</param> /// <param name="dataModelToStorageNameMapping">Storage property names keyed by data property names.</param> /// <param name="storageValues">A dictionary of storage values by storage property name.</param> /// <param name="storageValueConverter">An optional function to convert the storage property values to data property values.</param> public static void SetValuesOnProperties<TStorageType, TRecord>( TRecord record, IEnumerable<PropertyInfo> dataModelPropertiesInfo, IReadOnlyDictionary<string, string> dataModelToStorageNameMapping, IReadOnlyDictionary<string, TStorageType> storageValues, Func<TStorageType, Type, object?>? storageValueConverter = null) where TRecord : class { var propertiesInfoWithValues = BuildPropertiesInfoWithValues( dataModelPropertiesInfo, dataModelToStorageNameMapping, storageValues, storageValueConverter); SetPropertiesOnRecord(record, propertiesInfoWithValues); } /// <summary> /// Build a list of properties with their values from the given data model properties and storage values. /// </summary> /// <typeparam name="TStorageType">The type of the storage properties.</typeparam> /// <param name="dataModelPropertiesInfo"><see cref="PropertyInfo"/> objects listing the properties on the data model to get values for.</param> /// <param name="dataModelToStorageNameMapping">Storage property names keyed by data property names.</param> /// <param name="storageValues">A dictionary of storage values by storage property name.</param> /// <param name="storageValueConverter">An optional function to convert the storage property values to data property values.</param> /// <returns>The list of data property objects and their values.</returns> public static IEnumerable<KeyValuePair<PropertyInfo, object?>> BuildPropertiesInfoWithValues<TStorageType>( IEnumerable<PropertyInfo> dataModelPropertiesInfo, IReadOnlyDictionary<string, string> dataModelToStorageNameMapping, IReadOnlyDictionary<string, TStorageType> storageValues, Func<TStorageType, Type, object?>? storageValueConverter = null) { foreach (var propertyInfo in dataModelPropertiesInfo) { if (dataModelToStorageNameMapping.TryGetValue(propertyInfo.Name, out var storageName) && storageValues.TryGetValue(storageName, out var storageValue)) { if (storageValueConverter is not null) { var convertedStorageValue = storageValueConverter(storageValue, propertyInfo.PropertyType); yield return new KeyValuePair<PropertyInfo, object?>(propertyInfo, convertedStorageValue); } else { yield return new KeyValuePair<PropertyInfo, object?>(propertyInfo, (object?)storageValue); } } } } /// <summary> /// Set the given list of properties with their values on the given object. /// </summary> /// <typeparam name="TRecord">The type of the target object.</typeparam> /// <param name="record">The target object to set the property values on.</param> /// <param name="propertiesInfoWithValues">A list of properties and their values to set.</param> public static void SetPropertiesOnRecord<TRecord>( TRecord record, IEnumerable<KeyValuePair<PropertyInfo, object?>> propertiesInfoWithValues) where TRecord : class { foreach (var propertyInfoWithValue in propertiesInfoWithValues) { propertyInfoWithValue.Key.SetValue(record, propertyInfoWithValue.Value); } } /// <summary> /// Create an enumerable of the required type from the input enumerable. /// </summary> /// <typeparam name="T">The type of elements in the input enumerable.</typeparam> /// <param name="input">The input enumerable to convert.</param> /// <param name="requiredEnumerable">The type to convert to.</param> /// <returns>The new enumerable in the required type.</returns> /// <exception cref="NotSupportedException">Thrown when a target type is requested that is not supported.</exception> public static object? CreateEnumerable<T>(IEnumerable<T> input, Type requiredEnumerable) { if (input is null) { return null; } // If the required type is an array, we can create an ArrayList of the required type, add all // items from the input, and then convert the ArrayList to an array of the required type. if (requiredEnumerable.IsArray) { if (requiredEnumerable.HasElementType) { var elementType = requiredEnumerable.GetElementType(); var arrayList = new ArrayList(); foreach (var item in input) { arrayList.Add(item); } return arrayList.ToArray(elementType!); } return input.ToArray(); } // If the required type is one of a few supported generic collection interface types that // are all implemented by List<>, we can create a LIst<> and add all items from the input. if (requiredEnumerable.IsGenericType) { var genericTypeDefinition = requiredEnumerable.GetGenericTypeDefinition(); if (genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(IList<>) || genericTypeDefinition == typeof(IReadOnlyCollection<>) || genericTypeDefinition == typeof(IReadOnlyList<>)) { // Create a List<> using the generic type argument of the required enumerable. var genericMemberType = requiredEnumerable.GetGenericArguments()[0]; var listType = typeof(List<>).MakeGenericType(genericMemberType); var enumerableType = typeof(IEnumerable<>).MakeGenericType(genericMemberType); var constructor = listType.GetConstructor([]); var list = (IList)constructor!.Invoke(null); // Add all items from the input into the new list. foreach (var item in input) { list.Add(item); } return list; } } // If the required type is IEnumerable, we can return the input as is. if (requiredEnumerable == typeof(IEnumerable)) { return input; } // If our required type implements IList and has a public parameterless constructor, we can // create an instance of it using reflection and add all items from the input. if (typeof(IList).IsAssignableFrom(requiredEnumerable)) { var publicParameterlessConstructor = requiredEnumerable.GetConstructor([]); if (publicParameterlessConstructor is not null) { // Create the required type using the parameterless constructor and cast // it to an IList so we can add our input items. var list = (IList)publicParameterlessConstructor.Invoke(null); foreach (var item in input) { list.Add(item); } return list; } } throw new NotSupportedException($"Type {requiredEnumerable.FullName} is not supported."); } } #pragma warning restore IDE0004, IDE0059