forked from dotnet/extensions
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPropertyModel.cs
More file actions
166 lines (145 loc) · 6.37 KB
/
PropertyModel.cs
File metadata and controls
166 lines (145 loc) · 6.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// 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. If the property corresponds to a .NET property, this name is the name of that property.
/// </summary>
public string ModelName { get; set; } = modelName;
/// <summary>
/// Gets or sets the storage name of the property. This is the name to which the property is mapped in the vector store.
/// </summary>
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. 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
/// (i.e., POCO mapping); otherwise, reference types are assumed nullable.
/// </summary>
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);
}