├── .editorconfig ├── .gitattributes ├── .gitignore ├── EFCore.SqlServer.VectorSearch.Test ├── EFCore.SqlServer.VectorSearch.Test.csproj ├── TestUtilities │ ├── SqlServerDatabaseCleaner.cs │ ├── SqlServerDatabaseFacadeExtensions.cs │ ├── SqlServerTestStore.cs │ ├── SqlServerTestStoreFactory.cs │ ├── TestEnvironment.cs │ └── TestSqlServerRetryingExecutionStrategy.cs └── VectorSearchQueryTest.cs ├── EFCore.SqlServer.VectorSearch.sln ├── EFCore.SqlServer.VectorSearch ├── EFCore.SqlServer.VectorSearch.csproj ├── Extensions │ ├── SqlServerVectorSearchDbContextOptionsBuilderExtensions.cs │ ├── SqlServerVectorSearchDbFunctionsExtensions.cs │ ├── SqlServerVectorSearchPropertyBuilderExtensions.cs │ └── SqlServerVectorSearchServiceCollectionExtensions.cs ├── Infrastructure │ └── SqlServerVectorSearchOptionsExtension.cs ├── Query │ ├── SqlServerVectorSearchEvaluatableExpressionFilterPlugin.cs │ ├── SqlServerVectorSearchMethodCallTranslator.cs │ └── SqlServerVectorSearchMethodCallTranslatorPlugin.cs └── Storage │ ├── SqlServerVectorSearchTypeMappingSourcePlugin.cs │ └── SqlServerVectorTypeMapping.cs ├── LICENSE └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # Schema: http://EditorConfig.org 2 | # Docs: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Don't use tabs for indentation. 8 | [*] 9 | indent_style = space 10 | trim_trailing_whitespace = true 11 | guidelines = 140 12 | max_line_length = 140 13 | 14 | # Code files 15 | [*.{cs,csx,vb,vbx}] 16 | indent_size = 4 17 | insert_final_newline = true 18 | #charset = utf-8-bom 19 | 20 | # Xml project files 21 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 22 | indent_size = 2 23 | 24 | # Xml config files 25 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,xml,stylecop}] 26 | indent_size = 2 27 | 28 | # JSON files 29 | [*.json] 30 | indent_size = 2 31 | 32 | # Powershell files 33 | [*.ps1] 34 | indent_size = 2 35 | 36 | # Shell scripts 37 | [*.sh] 38 | end_of_line = lf 39 | indent_size = 2 40 | 41 | [*.{cmd,bat}] 42 | end_of_line = crlf 43 | indent_size = 2 44 | 45 | ## Language conventions 46 | # Dotnet code style settings: 47 | [*.{cs,vb}] 48 | # "This." and "Me." qualifiers 49 | dotnet_style_qualification_for_field = false:suggestion 50 | dotnet_style_qualification_for_property = false:suggestion 51 | dotnet_style_qualification_for_method = false:suggestion 52 | dotnet_style_qualification_for_event = false:suggestion 53 | 54 | # Language keywords instead of framework type names for type references 55 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 56 | dotnet_style_predefined_type_for_member_access = true:suggestion 57 | 58 | # Modifier preferences 59 | dotnet_style_require_accessibility_modifiers = always:suggestion 60 | dotnet_style_readonly_field = true:warning 61 | 62 | # Parentheses preferences 63 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 64 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 65 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 66 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 67 | 68 | # Expression-level preferences 69 | dotnet_style_object_initializer = true:suggestion 70 | dotnet_style_collection_initializer = true:suggestion 71 | dotnet_style_explicit_tuple_names = true:suggestion 72 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 73 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 74 | dotnet_style_prefer_auto_properties = true:silent 75 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 76 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 77 | 78 | # Null-checking preferences 79 | dotnet_style_coalesce_expression = true:suggestion 80 | dotnet_style_null_propagation = true:suggestion 81 | 82 | # CSharp code style settings: 83 | [*.cs] 84 | # Modifier preferences 85 | csharp_preferred_modifier_order = public,private,protected,internal,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 86 | 87 | # Implicit and explicit types 88 | csharp_style_var_for_built_in_types = true:suggestion 89 | csharp_style_var_when_type_is_apparent = true:suggestion 90 | csharp_style_var_elsewhere = true:suggestion 91 | 92 | # Expression-bodied members 93 | # Explicitly disabled due to difference in coding style between source and tests 94 | csharp_style_expression_bodied_methods = true:suggestion 95 | csharp_style_expression_bodied_constructors = true:suggestion 96 | csharp_style_expression_bodied_operators = true:suggestion 97 | csharp_style_expression_bodied_properties = true:suggestion 98 | csharp_style_expression_bodied_indexers = true:suggestion 99 | csharp_style_expression_bodied_accessors = true:suggestion 100 | 101 | # Pattern matching 102 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 103 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 104 | 105 | # Inlined variable declarations 106 | csharp_style_inlined_variable_declaration = true:suggestion 107 | 108 | # Expression-level preferences 109 | csharp_prefer_simple_default_expression = true:suggestion 110 | csharp_style_deconstructed_variable_declaration = true:suggestion 111 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 112 | 113 | # Null-checking preference 114 | csharp_style_throw_expression = true:suggestion 115 | csharp_style_conditional_delegate_call = true:suggestion 116 | 117 | # Code block preferences 118 | csharp_prefer_braces = true:suggestion 119 | 120 | # Primary constructors 121 | csharp_style_prefer_primary_constructors = true:suggestion 122 | 123 | ## Formatting conventions 124 | # Dotnet formatting settings: 125 | [*.{cs,vb}] 126 | # Organize usings 127 | dotnet_sort_system_directives_first = true 128 | dotnet_separate_import_directive_groups = false 129 | 130 | # CSharp formatting settings: 131 | [*.cs] 132 | # Newline options 133 | csharp_new_line_before_open_brace = all 134 | csharp_new_line_before_else = true 135 | csharp_new_line_before_catch = true 136 | csharp_new_line_before_finally = true 137 | csharp_new_line_before_members_in_object_initializers = true 138 | csharp_new_line_before_members_in_anonymous_types = true 139 | csharp_new_line_between_query_expression_clauses = true 140 | 141 | # Identation options 142 | csharp_indent_block_contents = true 143 | csharp_indent_braces = false 144 | csharp_indent_case_contents_when_block = false 145 | csharp_indent_switch_labels = true 146 | csharp_indent_case_contents = true 147 | csharp_indent_labels = no_change 148 | 149 | # Spacing options 150 | csharp_space_after_cast = false 151 | csharp_space_after_keywords_in_control_flow_statements = true 152 | csharp_space_between_method_declaration_parameter_list_parentheses = false 153 | csharp_space_between_method_call_parameter_list_parentheses = false 154 | csharp_space_between_parentheses = false 155 | csharp_space_before_colon_in_inheritance_clause = true 156 | csharp_space_after_colon_in_inheritance_clause = true 157 | csharp_space_around_binary_operators = before_and_after 158 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 159 | csharp_space_between_method_call_name_and_opening_parenthesis = false 160 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 161 | csharp_space_after_comma = true 162 | csharp_space_after_dot = false 163 | csharp_space_after_semicolon_in_for_statement = true 164 | csharp_space_around_declaration_statements = do_not_ignore 165 | csharp_space_before_comma = false 166 | csharp_space_before_dot = false 167 | csharp_space_before_open_square_brackets = false 168 | csharp_space_before_semicolon_in_for_statement = false 169 | csharp_space_between_empty_square_brackets = false 170 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 171 | csharp_space_between_square_brackets = false 172 | 173 | # Wrap options 174 | csharp_preserve_single_line_statements = true 175 | csharp_preserve_single_line_blocks = true 176 | 177 | ## Naming conventions 178 | [*.{cs,vb}] 179 | 180 | ## Naming styles 181 | 182 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 183 | dotnet_naming_style.camel_case_style.capitalization = camel_case 184 | 185 | # PascalCase with I prefix 186 | dotnet_naming_style.interface_style.capitalization = pascal_case 187 | dotnet_naming_style.interface_style.required_prefix = I 188 | 189 | # PascalCase with T prefix 190 | dotnet_naming_style.type_parameter_style.capitalization = pascal_case 191 | dotnet_naming_style.type_parameter_style.required_prefix = T 192 | 193 | # camelCase with _ prefix 194 | dotnet_naming_style._camelCase.capitalization = camel_case 195 | dotnet_naming_style._camelCase.required_prefix = _ 196 | 197 | ## Rules 198 | # Interfaces 199 | dotnet_naming_symbols.interface_symbol.applicable_kinds = interface 200 | dotnet_naming_symbols.interface_symbol.applicable_accessibilities = * 201 | dotnet_naming_rule.interface_naming.symbols = interface_symbol 202 | dotnet_naming_rule.interface_naming.style = interface_style 203 | dotnet_naming_rule.interface_naming.severity = suggestion 204 | 205 | # Classes, Structs, Enums, Properties, Methods, Local Functions, Events, Namespaces 206 | dotnet_naming_symbols.class_symbol.applicable_kinds = class, struct, enum, property, method, local_function, event, namespace, delegate 207 | dotnet_naming_symbols.class_symbol.applicable_accessibilities = * 208 | 209 | dotnet_naming_rule.class_naming.symbols = class_symbol 210 | dotnet_naming_rule.class_naming.style = pascal_case_style 211 | dotnet_naming_rule.class_naming.severity = suggestion 212 | 213 | # Type Parameters 214 | dotnet_naming_symbols.type_parameter_symbol.applicable_kinds = type_parameter 215 | dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = * 216 | 217 | dotnet_naming_rule.type_parameter_naming.symbols = type_parameter_symbol 218 | dotnet_naming_rule.type_parameter_naming.style = type_parameter_style 219 | dotnet_naming_rule.type_parameter_naming.severity = suggestion 220 | 221 | # Visible Fields 222 | dotnet_naming_symbols.public_field_symbol.applicable_kinds = field 223 | dotnet_naming_symbols.public_field_symbol.applicable_accessibilities = public, internal, protected, protected_internal, private_protected 224 | 225 | dotnet_naming_rule.public_field_naming.symbols = public_field_symbol 226 | dotnet_naming_rule.public_field_naming.style = pascal_case_style 227 | dotnet_naming_rule.public_field_naming.severity = suggestion 228 | 229 | # Private constant Fields 230 | dotnet_naming_symbols.const_field_symbol.applicable_kinds = field 231 | dotnet_naming_symbols.const_field_symbol.applicable_accessibilities = private 232 | dotnet_naming_symbols.const_field_symbol.required_modifiers = const 233 | 234 | dotnet_naming_rule.const_field_naming.symbols = const_field_symbol 235 | dotnet_naming_rule.const_field_naming.style = pascal_case_style 236 | dotnet_naming_rule.const_field_naming.severity = suggestion 237 | 238 | # Parameters 239 | dotnet_naming_symbols.parameter_symbol.applicable_kinds = parameter 240 | dotnet_naming_symbols.parameter_symbol.applicable_accessibilities = * 241 | 242 | dotnet_naming_rule.parameter_naming.symbols = parameter_symbol 243 | dotnet_naming_rule.parameter_naming.style = camel_case_style 244 | dotnet_naming_rule.parameter_naming.severity = suggestion 245 | 246 | # Everything Local 247 | dotnet_naming_symbols.everything_else.applicable_kinds = local 248 | dotnet_naming_symbols.everything_else.applicable_accessibilities = * 249 | 250 | dotnet_naming_rule.everything_else_naming.symbols = everything_else 251 | dotnet_naming_rule.everything_else_naming.style = camel_case_style 252 | dotnet_naming_rule.everything_else_naming.severity = suggestion 253 | 254 | # ReSharper properties 255 | resharper_local_function_body = expression_body 256 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.cs text=auto diff=csharp 4 | *.csproj text=auto 5 | *.sln text=auto 6 | *.resx text=auto 7 | *.xml text=auto 8 | *.txt text=auto 9 | 10 | packages/ binary 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.resources 3 | *.suo 4 | *.user 5 | *.sln.docstates 6 | *.userprefs 7 | /*.nupkg 8 | .nuget/ 9 | .idea/ 10 | [Bb]in/ 11 | [Bb]uild/ 12 | [Oo]bj/ 13 | [Oo]bj/ 14 | packages/*/ 15 | Packages/*/ 16 | packages.stable 17 | artifacts/ 18 | # Roslyn cache directories 19 | *.ide/ 20 | .vs/ 21 | TestResult.xml 22 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/EFCore.SqlServer.VectorSearch.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/TestUtilities/SqlServerDatabaseCleaner.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.EntityFrameworkCore.Migrations.Operations; 5 | using Microsoft.EntityFrameworkCore.Scaffolding; 6 | using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; 7 | using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal; 8 | using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | 12 | // ReSharper disable once CheckNamespace 13 | namespace Microsoft.EntityFrameworkCore.TestUtilities; 14 | 15 | #nullable disable 16 | #pragma warning disable EF1001 // Internal EF Core API usage. 17 | // Copied from EF 18 | 19 | public class SqlServerDatabaseCleaner : RelationalDatabaseCleaner 20 | { 21 | protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) 22 | { 23 | var services = new ServiceCollection(); 24 | services.AddEntityFrameworkSqlServer(); 25 | 26 | new SqlServerDesignTimeServices().ConfigureDesignTimeServices(services); 27 | 28 | return services 29 | .BuildServiceProvider() // No scope validation; cleaner violates scopes, but only resolve services once. 30 | .GetRequiredService(); 31 | } 32 | 33 | protected override bool AcceptTable(DatabaseTable table) 34 | => table is not DatabaseView; 35 | 36 | protected override bool AcceptIndex(DatabaseIndex index) 37 | => false; 38 | 39 | private readonly string _dropViewsSql = @" 40 | DECLARE @name varchar(max) = '__dummy__', @SQL varchar(max) = ''; 41 | 42 | WHILE @name IS NOT NULL 43 | BEGIN 44 | SELECT @name = 45 | (SELECT TOP 1 QUOTENAME(s.[name]) + '.' + QUOTENAME(o.[name]) 46 | FROM sysobjects o 47 | INNER JOIN sys.views v ON o.id = v.object_id 48 | INNER JOIN sys.schemas s ON s.schema_id = v.schema_id 49 | WHERE (s.name = 'dbo' OR s.principal_id <> s.schema_id) AND o.[type] = 'V' AND o.category = 0 AND o.[name] NOT IN 50 | ( 51 | SELECT referenced_entity_name 52 | FROM sys.sql_expression_dependencies AS sed 53 | INNER JOIN sys.objects AS o ON sed.referencing_id = o.object_id 54 | ) 55 | ORDER BY v.[name]) 56 | 57 | SELECT @SQL = 'DROP VIEW ' + @name 58 | EXEC (@SQL) 59 | END"; 60 | 61 | protected override string BuildCustomSql(DatabaseModel databaseModel) 62 | => _dropViewsSql; 63 | 64 | protected override string BuildCustomEndingSql(DatabaseModel databaseModel) 65 | => _dropViewsSql 66 | + @" 67 | GO 68 | 69 | DECLARE @SQL varchar(max) = ''; 70 | SELECT @SQL = @SQL + 'DROP FUNCTION ' + QUOTENAME(ROUTINE_SCHEMA) + '.' + QUOTENAME(ROUTINE_NAME) + ';' 71 | FROM [INFORMATION_SCHEMA].[ROUTINES] WHERE ROUTINE_TYPE = 'FUNCTION' AND ROUTINE_BODY = 'SQL'; 72 | EXEC (@SQL); 73 | 74 | SET @SQL =''; 75 | SELECT @SQL = @SQL + 'DROP AGGREGATE ' + QUOTENAME(ROUTINE_SCHEMA) + '.' + QUOTENAME(ROUTINE_NAME) + ';' 76 | FROM [INFORMATION_SCHEMA].[ROUTINES] WHERE ROUTINE_TYPE = 'FUNCTION' AND ROUTINE_BODY = 'EXTERNAL'; 77 | EXEC (@SQL); 78 | 79 | SET @SQL =''; 80 | SELECT @SQL = @SQL + 'DROP PROC ' + QUOTENAME(schema_name(schema_id)) + '.' + QUOTENAME(name) + ';' FROM sys.procedures; 81 | EXEC (@SQL); 82 | 83 | SET @SQL =''; 84 | SELECT @SQL = @SQL + 'DROP TYPE ' + QUOTENAME(schema_name(schema_id)) + '.' + QUOTENAME(name) + ';' FROM sys.types WHERE is_user_defined = 1; 85 | EXEC (@SQL); 86 | 87 | SET @SQL =''; 88 | SELECT @SQL = @SQL + 'DROP SCHEMA ' + QUOTENAME(name) + ';' FROM sys.schemas WHERE principal_id <> schema_id; 89 | EXEC (@SQL);"; 90 | 91 | protected override MigrationOperation Drop(DatabaseTable table) 92 | => AddSqlServerSpecificAnnotations(base.Drop(table), table); 93 | 94 | protected override MigrationOperation Drop(DatabaseForeignKey foreignKey) 95 | => AddSqlServerSpecificAnnotations(base.Drop(foreignKey), foreignKey.Table); 96 | 97 | protected override MigrationOperation Drop(DatabaseIndex index) 98 | => AddSqlServerSpecificAnnotations(base.Drop(index), index.Table); 99 | 100 | private static TOperation AddSqlServerSpecificAnnotations(TOperation operation, DatabaseTable table) 101 | where TOperation : MigrationOperation 102 | { 103 | operation[SqlServerAnnotationNames.MemoryOptimized] 104 | = table[SqlServerAnnotationNames.MemoryOptimized] as bool?; 105 | 106 | if (table[SqlServerAnnotationNames.IsTemporal] != null) 107 | { 108 | operation[SqlServerAnnotationNames.IsTemporal] 109 | = table[SqlServerAnnotationNames.IsTemporal]; 110 | 111 | operation[SqlServerAnnotationNames.TemporalHistoryTableName] 112 | = table[SqlServerAnnotationNames.TemporalHistoryTableName]; 113 | 114 | operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] 115 | = table[SqlServerAnnotationNames.TemporalHistoryTableSchema]; 116 | 117 | operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] 118 | = table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]; 119 | 120 | operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] 121 | = table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]; 122 | } 123 | 124 | return operation; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/TestUtilities/SqlServerDatabaseFacadeExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | 6 | // ReSharper disable once CheckNamespace 7 | namespace Microsoft.EntityFrameworkCore.TestUtilities; 8 | 9 | // Copied from EF 10 | 11 | public static class SqlServerDatabaseFacadeExtensions 12 | { 13 | public static void EnsureClean(this DatabaseFacade databaseFacade) 14 | => databaseFacade.CreateExecutionStrategy() 15 | .Execute(databaseFacade, database => new SqlServerDatabaseCleaner().Clean(database)); 16 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/TestUtilities/SqlServerTestStore.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Data; 5 | using System.Data.Common; 6 | using System.Runtime.CompilerServices; 7 | using System.Text.RegularExpressions; 8 | using Microsoft.Data.SqlClient; 9 | using Microsoft.EntityFrameworkCore.Diagnostics; 10 | 11 | #pragma warning disable IDE0022 // Use block body for methods 12 | // ReSharper disable once CheckNamespace 13 | namespace Microsoft.EntityFrameworkCore.TestUtilities; 14 | 15 | // Copied from EF 16 | 17 | public class SqlServerTestStore : RelationalTestStore 18 | { 19 | public const int CommandTimeout = 300; 20 | 21 | private static string CurrentDirectory 22 | => Environment.CurrentDirectory; 23 | 24 | public static SqlServerTestStore GetOrCreate(string name) 25 | => new(name); 26 | 27 | public static async Task GetOrCreateInitializedAsync(string name) 28 | => await new SqlServerTestStore(name).InitializeSqlServerAsync(null, (Func?)null, null); 29 | 30 | public static SqlServerTestStore GetOrCreateWithInitScript(string name, string initScript) 31 | => new(name, initScript: initScript); 32 | 33 | public static SqlServerTestStore GetOrCreateWithScriptPath( 34 | string name, 35 | string scriptPath, 36 | bool? multipleActiveResultSets = null, 37 | bool shared = true) 38 | => new(name, scriptPath: scriptPath, multipleActiveResultSets: multipleActiveResultSets, shared: shared); 39 | 40 | public static SqlServerTestStore Create(string name, bool useFileName = false) 41 | => new(name, useFileName, shared: false); 42 | 43 | public static async Task CreateInitializedAsync( 44 | string name, 45 | bool useFileName = false, 46 | bool? multipleActiveResultSets = null) 47 | => await new SqlServerTestStore(name, useFileName, shared: false, multipleActiveResultSets: multipleActiveResultSets) 48 | .InitializeSqlServerAsync(null, (Func?)null, null); 49 | 50 | private readonly string? _fileName; 51 | private readonly string? _initScript; 52 | private readonly string? _scriptPath; 53 | 54 | protected SqlServerTestStore( 55 | string name, 56 | bool useFileName = false, 57 | bool? multipleActiveResultSets = null, 58 | string? initScript = null, 59 | string? scriptPath = null, 60 | bool shared = true) 61 | : base(name, shared, CreateConnection(name, useFileName, multipleActiveResultSets)) 62 | { 63 | _fileName = GenerateFileName(useFileName, name); 64 | 65 | if (initScript != null) 66 | { 67 | _initScript = initScript; 68 | } 69 | 70 | if (scriptPath != null) 71 | { 72 | _scriptPath = Path.Combine(Path.GetDirectoryName(typeof(SqlServerTestStore).Assembly.Location)!, scriptPath); 73 | } 74 | } 75 | 76 | public async Task InitializeSqlServerAsync( 77 | IServiceProvider? serviceProvider, 78 | Func? createContext, 79 | Func? seed) 80 | => (SqlServerTestStore)await InitializeAsync(serviceProvider, createContext, seed); 81 | 82 | public async Task InitializeSqlServerAsync( 83 | IServiceProvider serviceProvider, 84 | Func createContext, 85 | Func seed) 86 | => await InitializeSqlServerAsync(serviceProvider, () => createContext(this), seed); 87 | 88 | protected override async Task InitializeAsync(Func createContext, Func? seed, Func? clean) 89 | { 90 | if (await CreateDatabaseAsync(clean)) 91 | { 92 | if (_scriptPath != null) 93 | { 94 | ExecuteScript(await File.ReadAllTextAsync(_scriptPath)); 95 | } 96 | else 97 | { 98 | using var context = createContext(); 99 | await context.Database.EnsureCreatedResilientlyAsync(); 100 | 101 | if (_initScript != null) 102 | { 103 | ExecuteScript(_initScript); 104 | } 105 | 106 | if (seed != null) 107 | { 108 | await seed(context); 109 | } 110 | } 111 | } 112 | } 113 | public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) 114 | => builder.UseSqlServer(Connection); 115 | 116 | //public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) 117 | // => (UseConnectionString 118 | // ? builder.UseSqlServer(ConnectionString, b => b.ApplyConfiguration()) 119 | // : builder.UseSqlServer(Connection, b => b.ApplyConfiguration())) 120 | // .ConfigureWarnings(b => b.Ignore(SqlServerEventId.SavepointsDisabledBecauseOfMARS)); 121 | 122 | private async Task CreateDatabaseAsync(Func? clean) 123 | { 124 | await using var master = new SqlConnection(CreateConnectionString("master", fileName: null, multipleActiveResultSets: false)); 125 | 126 | if (ExecuteScalar(master, $"SELECT COUNT(*) FROM sys.databases WHERE name = N'{Name}'") > 0) 127 | { 128 | // Only reseed scripted databases during CI runs 129 | if (_scriptPath != null && !TestEnvironment.IsCI) 130 | { 131 | return false; 132 | } 133 | 134 | if (_fileName == null) 135 | { 136 | await using var context = new DbContext( 137 | AddProviderOptions(new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options); 138 | await CleanAsync(context); 139 | 140 | if (clean != null) 141 | { 142 | await clean(context); 143 | } 144 | 145 | return true; 146 | } 147 | 148 | // Delete the database to ensure it's recreated with the correct file path 149 | await DeleteDatabaseAsync(); 150 | } 151 | 152 | await ExecuteNonQueryAsync(master, GetCreateDatabaseStatement(Name, _fileName)); 153 | await WaitForExistsAsync((SqlConnection)Connection); 154 | 155 | return true; 156 | } 157 | 158 | public override Task CleanAsync(DbContext context) 159 | { 160 | context.Database.EnsureClean(); 161 | return Task.CompletedTask; 162 | } 163 | 164 | public void ExecuteScript(string script) 165 | => Execute( 166 | Connection, command => 167 | { 168 | foreach (var batch in RelationalDatabaseCleaner.SplitBatches(script)) 169 | { 170 | command.CommandText = batch; 171 | command.ExecuteNonQuery(); 172 | } 173 | 174 | return 0; 175 | }, ""); 176 | 177 | private static Task WaitForExistsAsync(SqlConnection connection) 178 | => new TestSqlServerRetryingExecutionStrategy().ExecuteAsync(connection, WaitForExistsImplementation); 179 | 180 | private static async Task WaitForExistsImplementation(SqlConnection connection) 181 | { 182 | var retryCount = 0; 183 | while (true) 184 | { 185 | try 186 | { 187 | if (connection.State != ConnectionState.Closed) 188 | { 189 | await connection.CloseAsync(); 190 | } 191 | 192 | SqlConnection.ClearPool(connection); 193 | 194 | await connection.OpenAsync(); 195 | await connection.CloseAsync(); 196 | return; 197 | } 198 | catch (SqlException e) 199 | { 200 | if (++retryCount >= 30 201 | || e.Number != 233 && e.Number != -2 && e.Number != 4060 && e.Number != 1832 && e.Number != 5120) 202 | { 203 | throw; 204 | } 205 | 206 | await Task.Delay(100); 207 | } 208 | } 209 | } 210 | 211 | private static string GetCreateDatabaseStatement(string name, string? fileName) 212 | { 213 | var result = $"CREATE DATABASE [{name}]"; 214 | 215 | if (TestEnvironment.IsSqlAzure) 216 | { 217 | var elasticGroupName = TestEnvironment.ElasticPoolName; 218 | result += Environment.NewLine 219 | + (string.IsNullOrEmpty(elasticGroupName) 220 | ? " ( Edition = 'basic' )" 221 | : $" ( SERVICE_OBJECTIVE = ELASTIC_POOL ( name = {elasticGroupName} ) )"); 222 | } 223 | else 224 | { 225 | if (!string.IsNullOrEmpty(fileName)) 226 | { 227 | var logFileName = Path.ChangeExtension(fileName, ".ldf"); 228 | result += Environment.NewLine 229 | + $" ON (NAME = '{name}', FILENAME = '{fileName}')" 230 | + $" LOG ON (NAME = '{name}_log', FILENAME = '{logFileName}')"; 231 | } 232 | } 233 | 234 | return result; 235 | } 236 | 237 | public async Task DeleteDatabaseAsync() 238 | { 239 | await using var master = new SqlConnection(CreateConnectionString("master")); 240 | 241 | await ExecuteNonQueryAsync( 242 | master, string.Format( 243 | """ 244 | IF EXISTS (SELECT * FROM sys.databases WHERE name = N'{0}') 245 | BEGIN 246 | ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; 247 | DROP DATABASE [{0}]; 248 | END 249 | """, Name)); 250 | 251 | SqlConnection.ClearAllPools(); 252 | } 253 | 254 | public override void OpenConnection() 255 | => new TestSqlServerRetryingExecutionStrategy().Execute(Connection, connection => connection.Open()); 256 | 257 | public override Task OpenConnectionAsync() 258 | => new TestSqlServerRetryingExecutionStrategy().ExecuteAsync(Connection, connection => connection.OpenAsync()); 259 | 260 | public T ExecuteScalar(string sql, params object[] parameters) 261 | => ExecuteScalar(Connection, sql, parameters); 262 | 263 | private static T ExecuteScalar(DbConnection connection, string sql, params object[] parameters) 264 | => Execute(connection, command => (T)command.ExecuteScalar()!, sql, false, parameters); 265 | 266 | public Task ExecuteScalarAsync(string sql, params object[] parameters) 267 | => ExecuteScalarAsync(Connection, sql, parameters); 268 | 269 | private static Task ExecuteScalarAsync(DbConnection connection, string sql, IReadOnlyList? parameters = null) 270 | => ExecuteAsync(connection, async command => (T)(await command.ExecuteScalarAsync())!, sql, false, parameters); 271 | 272 | public int ExecuteNonQuery(string sql, params object[] parameters) 273 | => ExecuteNonQuery(Connection, sql, parameters); 274 | 275 | private static int ExecuteNonQuery(DbConnection connection, string sql, object[]? parameters = null) 276 | => Execute(connection, command => command.ExecuteNonQuery(), sql, false, parameters); 277 | 278 | public Task ExecuteNonQueryAsync(string sql, params object[] parameters) 279 | => ExecuteNonQueryAsync(Connection, sql, parameters); 280 | 281 | private static Task ExecuteNonQueryAsync(DbConnection connection, string sql, IReadOnlyList? parameters = null) 282 | => ExecuteAsync(connection, command => command.ExecuteNonQueryAsync(), sql, false, parameters); 283 | 284 | public IEnumerable Query(string sql, params object[] parameters) 285 | => Query(Connection, sql, parameters); 286 | 287 | private static IEnumerable Query(DbConnection connection, string sql, object[]? parameters = null) 288 | => Execute( 289 | connection, command => 290 | { 291 | using var dataReader = command.ExecuteReader(); 292 | var results = Enumerable.Empty(); 293 | while (dataReader.Read()) 294 | { 295 | results = results.Concat(new[] { dataReader.GetFieldValue(0) }); 296 | } 297 | 298 | return results; 299 | }, sql, false, parameters); 300 | 301 | public Task> QueryAsync(string sql, params object[] parameters) 302 | => QueryAsync(Connection, sql, parameters); 303 | 304 | private static Task> QueryAsync(DbConnection connection, string sql, object[]? parameters = null) 305 | => ExecuteAsync( 306 | connection, async command => 307 | { 308 | using var dataReader = await command.ExecuteReaderAsync(); 309 | var results = Enumerable.Empty(); 310 | while (await dataReader.ReadAsync()) 311 | { 312 | results = results.Concat(new[] { await dataReader.GetFieldValueAsync(0) }); 313 | } 314 | 315 | return results; 316 | }, sql, false, parameters); 317 | 318 | private static T Execute( 319 | DbConnection connection, 320 | Func execute, 321 | string sql, 322 | bool useTransaction = false, 323 | object[]? parameters = null) 324 | => new TestSqlServerRetryingExecutionStrategy().Execute( 325 | new 326 | { 327 | connection, 328 | execute, 329 | sql, 330 | useTransaction, 331 | parameters 332 | }, 333 | state => ExecuteCommand(state.connection, state.execute, state.sql, state.useTransaction, state.parameters)); 334 | 335 | private static T ExecuteCommand( 336 | DbConnection connection, 337 | Func execute, 338 | string sql, 339 | bool useTransaction, 340 | object[]? parameters) 341 | { 342 | if (connection.State != ConnectionState.Closed) 343 | { 344 | connection.Close(); 345 | } 346 | 347 | connection.Open(); 348 | try 349 | { 350 | using var transaction = useTransaction ? connection.BeginTransaction() : null; 351 | T result; 352 | using (var command = CreateCommand(connection, sql, parameters)) 353 | { 354 | command.Transaction = transaction; 355 | result = execute(command); 356 | } 357 | 358 | transaction?.Commit(); 359 | 360 | return result; 361 | } 362 | finally 363 | { 364 | if (connection.State != ConnectionState.Closed) 365 | { 366 | connection.Close(); 367 | } 368 | } 369 | } 370 | 371 | private static Task ExecuteAsync( 372 | DbConnection connection, 373 | Func> executeAsync, 374 | string sql, 375 | bool useTransaction = false, 376 | IReadOnlyList? parameters = null) 377 | => new TestSqlServerRetryingExecutionStrategy().ExecuteAsync( 378 | new 379 | { 380 | connection, 381 | executeAsync, 382 | sql, 383 | useTransaction, 384 | parameters 385 | }, 386 | state => ExecuteCommandAsync(state.connection, state.executeAsync, state.sql, state.useTransaction, state.parameters)); 387 | 388 | private static async Task ExecuteCommandAsync( 389 | DbConnection connection, 390 | Func> executeAsync, 391 | string sql, 392 | bool useTransaction, 393 | IReadOnlyList? parameters) 394 | { 395 | if (connection.State != ConnectionState.Closed) 396 | { 397 | await connection.CloseAsync(); 398 | } 399 | 400 | await connection.OpenAsync(); 401 | try 402 | { 403 | using var transaction = useTransaction ? await connection.BeginTransactionAsync() : null; 404 | T result; 405 | using (var command = CreateCommand(connection, sql, parameters)) 406 | { 407 | result = await executeAsync(command); 408 | } 409 | 410 | if (transaction != null) 411 | { 412 | await transaction.CommitAsync(); 413 | } 414 | 415 | return result; 416 | } 417 | finally 418 | { 419 | if (connection.State != ConnectionState.Closed) 420 | { 421 | await connection.CloseAsync(); 422 | } 423 | } 424 | } 425 | 426 | private static DbCommand CreateCommand( 427 | DbConnection connection, 428 | string commandText, 429 | IReadOnlyList? parameters = null) 430 | { 431 | var command = (SqlCommand)connection.CreateCommand(); 432 | 433 | command.CommandText = commandText; 434 | command.CommandTimeout = CommandTimeout; 435 | 436 | if (parameters != null) 437 | { 438 | for (var i = 0; i < parameters.Count; i++) 439 | { 440 | command.Parameters.AddWithValue("p" + i, parameters[i]); 441 | } 442 | } 443 | 444 | return command; 445 | } 446 | 447 | public override async Task DisposeAsync() 448 | { 449 | await base.DisposeAsync(); 450 | 451 | if (_fileName != null // Clean up the database using a local file, as it might get deleted later 452 | || (TestEnvironment.IsSqlAzure && !Shared)) 453 | { 454 | await DeleteDatabaseAsync(); 455 | } 456 | } 457 | 458 | private static SqlConnection CreateConnection(string name, bool useFileName, bool? multipleActiveResultSets = null) 459 | { 460 | var connectionString = CreateConnectionString(name, GenerateFileName(useFileName, name), multipleActiveResultSets); 461 | return new SqlConnection(connectionString); 462 | } 463 | 464 | public static string CreateConnectionString(string name, string? fileName = null, bool? multipleActiveResultSets = null) 465 | { 466 | var builder = new SqlConnectionStringBuilder(TestEnvironment.DefaultConnection) 467 | { 468 | MultipleActiveResultSets = multipleActiveResultSets ?? Random.Shared.Next(0, 2) == 1, 469 | InitialCatalog = name 470 | }; 471 | if (fileName != null) 472 | { 473 | builder.AttachDBFilename = fileName; 474 | } 475 | 476 | return builder.ToString(); 477 | } 478 | 479 | private static string? GenerateFileName(bool useFileName, string name) 480 | => useFileName ? Path.Combine(CurrentDirectory, name + ".mdf") : null; 481 | } 482 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/TestUtilities/SqlServerTestStoreFactory.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | // ReSharper disable once CheckNamespace 9 | namespace Microsoft.EntityFrameworkCore.TestUtilities; 10 | 11 | // Copied from EF 12 | 13 | public class SqlServerTestStoreFactory : RelationalTestStoreFactory 14 | { 15 | public static SqlServerTestStoreFactory Instance { get; } = new(); 16 | 17 | protected SqlServerTestStoreFactory() 18 | { 19 | } 20 | 21 | public override TestStore Create(string storeName) 22 | => SqlServerTestStore.Create(storeName); 23 | 24 | public override TestStore GetOrCreate(string storeName) 25 | => SqlServerTestStore.GetOrCreate(storeName); 26 | 27 | public override IServiceCollection AddProviderServices(IServiceCollection serviceCollection) 28 | => serviceCollection 29 | .AddEntityFrameworkSqlServer() 30 | .AddEntityFrameworkSqlServerVectorSearch(); 31 | } 32 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/TestUtilities/TestEnvironment.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Data.SqlClient; 5 | using Microsoft.Extensions.Configuration; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace Microsoft.EntityFrameworkCore.TestUtilities; 9 | 10 | // Copied from EF 11 | 12 | public static class TestEnvironment 13 | { 14 | public static IConfiguration Config { get; } = new ConfigurationBuilder() 15 | .SetBasePath(Directory.GetCurrentDirectory()) 16 | .AddJsonFile("config.json", optional: true) 17 | .AddJsonFile("config.test.json", optional: true) 18 | .AddEnvironmentVariables() 19 | .Build() 20 | .GetSection("Test:SqlServer"); 21 | 22 | public static string DefaultConnection { get; } = Config["DefaultConnection"] 23 | ?? throw new Exception("A connection to a SQL Azure instance with vector search support must be configured."); 24 | 25 | private static readonly string _dataSource = new SqlConnectionStringBuilder(DefaultConnection).DataSource; 26 | 27 | public static bool IsConfigured { get; } = !string.IsNullOrEmpty(_dataSource); 28 | 29 | public static bool IsCI { get; } = Environment.GetEnvironmentVariable("CI") != null 30 | || Environment.GetEnvironmentVariable("PIPELINE_WORKSPACE") != null; 31 | 32 | private static bool? _isAzureSqlDb; 33 | 34 | private static bool? _supportsFunctions2017; 35 | 36 | private static bool? _supportsFunctions2019; 37 | 38 | private static bool? _supportsFunctions2022; 39 | 40 | private static byte? _productMajorVersion; 41 | 42 | private static int? _engineEdition; 43 | 44 | public static bool IsSqlAzure 45 | { 46 | get 47 | { 48 | if (!IsConfigured) 49 | { 50 | return false; 51 | } 52 | 53 | if (_isAzureSqlDb.HasValue) 54 | { 55 | return _isAzureSqlDb.Value; 56 | } 57 | 58 | try 59 | { 60 | _isAzureSqlDb = GetEngineEdition() is 5 or 8; 61 | } 62 | catch (PlatformNotSupportedException) 63 | { 64 | _isAzureSqlDb = false; 65 | } 66 | 67 | return _isAzureSqlDb.Value; 68 | } 69 | } 70 | 71 | public static bool IsLocalDb { get; } = _dataSource.StartsWith("(localdb)", StringComparison.OrdinalIgnoreCase); 72 | 73 | public static bool IsFunctions2017Supported 74 | { 75 | get 76 | { 77 | if (!IsConfigured) 78 | { 79 | return false; 80 | } 81 | 82 | if (_supportsFunctions2017.HasValue) 83 | { 84 | return _supportsFunctions2017.Value; 85 | } 86 | 87 | try 88 | { 89 | _supportsFunctions2017 = GetProductMajorVersion() >= 14 || IsSqlAzure; 90 | } 91 | catch (PlatformNotSupportedException) 92 | { 93 | _supportsFunctions2017 = false; 94 | } 95 | 96 | return _supportsFunctions2017.Value; 97 | } 98 | } 99 | 100 | public static bool IsFunctions2019Supported 101 | { 102 | get 103 | { 104 | if (!IsConfigured) 105 | { 106 | return false; 107 | } 108 | 109 | if (_supportsFunctions2019.HasValue) 110 | { 111 | return _supportsFunctions2019.Value; 112 | } 113 | 114 | try 115 | { 116 | _supportsFunctions2019 = GetProductMajorVersion() >= 15 || IsSqlAzure; 117 | } 118 | catch (PlatformNotSupportedException) 119 | { 120 | _supportsFunctions2019 = false; 121 | } 122 | 123 | return _supportsFunctions2019.Value; 124 | } 125 | } 126 | 127 | public static bool IsFunctions2022Supported 128 | { 129 | get 130 | { 131 | if (!IsConfigured) 132 | { 133 | return false; 134 | } 135 | 136 | if (_supportsFunctions2022.HasValue) 137 | { 138 | return _supportsFunctions2022.Value; 139 | } 140 | 141 | try 142 | { 143 | _supportsFunctions2022 = GetProductMajorVersion() >= 16 || IsSqlAzure; 144 | } 145 | catch (PlatformNotSupportedException) 146 | { 147 | _supportsFunctions2022 = false; 148 | } 149 | 150 | return _supportsFunctions2022.Value; 151 | } 152 | } 153 | 154 | public static byte SqlServerMajorVersion 155 | => GetProductMajorVersion(); 156 | 157 | public static string? ElasticPoolName { get; } = Config["ElasticPoolName"]; 158 | 159 | public static bool? GetFlag(string key) 160 | => bool.TryParse(Config[key], out var flag) ? flag : null; 161 | 162 | public static int? GetInt(string key) 163 | => int.TryParse(Config[key], out var value) ? value : null; 164 | 165 | private static int GetEngineEdition() 166 | { 167 | if (_engineEdition.HasValue) 168 | { 169 | return _engineEdition.Value; 170 | } 171 | 172 | using var sqlConnection = new SqlConnection(SqlServerTestStore.CreateConnectionString("master")); 173 | sqlConnection.Open(); 174 | 175 | using var command = new SqlCommand( 176 | "SELECT SERVERPROPERTY('EngineEdition');", sqlConnection); 177 | _engineEdition = (int)command.ExecuteScalar(); 178 | 179 | return _engineEdition.Value; 180 | } 181 | 182 | private static byte GetProductMajorVersion() 183 | { 184 | if (_productMajorVersion.HasValue) 185 | { 186 | return _productMajorVersion.Value; 187 | } 188 | 189 | using var sqlConnection = new SqlConnection(SqlServerTestStore.CreateConnectionString("master")); 190 | sqlConnection.Open(); 191 | 192 | using var command = new SqlCommand( 193 | "SELECT SERVERPROPERTY('ProductVersion');", sqlConnection); 194 | _productMajorVersion = (byte)Version.Parse((string)command.ExecuteScalar()).Major; 195 | 196 | return _productMajorVersion.Value; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Data.SqlClient; 5 | using Microsoft.EntityFrameworkCore.Storage; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace Microsoft.EntityFrameworkCore.TestUtilities; 9 | 10 | // Copied from EF 11 | 12 | public class TestSqlServerRetryingExecutionStrategy : SqlServerRetryingExecutionStrategy 13 | { 14 | private const bool ErrorNumberDebugMode = false; 15 | 16 | private static readonly int[] _additionalErrorNumbers = 17 | { 18 | -1, // Physical connection is not usable 19 | -2, // Timeout 20 | 42008, // Mirroring (Only when a database is deleted and another one is created in fast succession) 21 | 42019 // CREATE DATABASE operation failed 22 | }; 23 | 24 | public TestSqlServerRetryingExecutionStrategy() 25 | : base( 26 | new DbContext( 27 | new DbContextOptionsBuilder() 28 | .EnableServiceProviderCaching(false) 29 | .UseSqlServer(TestEnvironment.DefaultConnection).Options), 30 | DefaultMaxRetryCount, DefaultMaxDelay, _additionalErrorNumbers) 31 | { 32 | } 33 | 34 | public TestSqlServerRetryingExecutionStrategy(DbContext context) 35 | : base(context, DefaultMaxRetryCount, DefaultMaxDelay, _additionalErrorNumbers) 36 | { 37 | } 38 | 39 | public TestSqlServerRetryingExecutionStrategy(DbContext context, TimeSpan maxDelay) 40 | : base(context, DefaultMaxRetryCount, maxDelay, _additionalErrorNumbers) 41 | { 42 | } 43 | 44 | public TestSqlServerRetryingExecutionStrategy(ExecutionStrategyDependencies dependencies) 45 | : base(dependencies, DefaultMaxRetryCount, DefaultMaxDelay, _additionalErrorNumbers) 46 | { 47 | } 48 | 49 | protected override bool ShouldRetryOn(Exception exception) 50 | { 51 | if (base.ShouldRetryOn(exception)) 52 | { 53 | return true; 54 | } 55 | 56 | if (ErrorNumberDebugMode 57 | && exception is SqlException sqlException) 58 | { 59 | var message = "Didn't retry on"; 60 | foreach (SqlError err in sqlException.Errors) 61 | { 62 | message += " " + err.Number; 63 | } 64 | 65 | message += Environment.NewLine; 66 | throw new InvalidOperationException(message + exception, exception); 67 | } 68 | 69 | return exception is InvalidOperationException { Message: "Internal .Net Framework Data Provider error 6." }; 70 | } 71 | 72 | public new virtual TimeSpan? GetNextDelay(Exception lastException) 73 | { 74 | ExceptionsEncountered.Add(lastException); 75 | return base.GetNextDelay(lastException); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.Test/VectorSearchQueryTest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.TestUtilities; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace EFCore.SqlServer.VectorSearch.Test; 8 | 9 | public class VectorSearchQueryTest 10 | : IClassFixture 11 | { 12 | private VectorSearchQueryFixture Fixture { get; } 13 | 14 | public VectorSearchQueryTest(VectorSearchQueryFixture fixture, ITestOutputHelper testOutputHelper) 15 | { 16 | Fixture = fixture; 17 | Fixture.TestSqlLoggerFactory.Clear(); 18 | Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); 19 | } 20 | 21 | [ConditionalFact] 22 | public virtual async Task VectorDistance_with_constant() 23 | { 24 | await using var context = CreateContext(); 25 | 26 | _ = await context.Products 27 | .OrderBy(p => EF.Functions.VectorDistance("cosine", p.Embedding, new[] { 1f, 2f, 3f })) 28 | .Take(1) 29 | .ToArrayAsync(); 30 | 31 | AssertSql( 32 | """ 33 | @__p_1='1' 34 | 35 | SELECT TOP(@__p_1) [p].[Id], [p].[Embedding] 36 | FROM [Products] AS [p] 37 | ORDER BY VECTOR_DISTANCE('cosine', [p].[Embedding], CAST('[1,2,3]' AS vector(3))) 38 | """); 39 | } 40 | 41 | [ConditionalFact] 42 | public virtual async Task VectorDistance_with_parameter() 43 | { 44 | await using var context = CreateContext(); 45 | 46 | var vector = new[] { 1f, 2f, 3f }; 47 | _ = await context.Products 48 | .OrderBy(p => EF.Functions.VectorDistance("dot", p.Embedding, vector)) 49 | .Take(1) 50 | .ToArrayAsync(); 51 | 52 | AssertSql( 53 | """ 54 | @__p_2='1' 55 | @__vector_1='[1,2,3]' (Size = 7) 56 | 57 | SELECT TOP(@__p_2) [p].[Id], [p].[Embedding] 58 | FROM [Products] AS [p] 59 | ORDER BY VECTOR_DISTANCE('dot', [p].[Embedding], CAST(@__vector_1 AS vector(3))) 60 | """); 61 | } 62 | 63 | [ConditionalFact] 64 | public virtual async Task Select_vector_out() 65 | { 66 | await using var context = CreateContext(); 67 | 68 | var serverVector = await context.Products.Where(p => p.Id == 1).Select(p => p.Embedding).SingleAsync(); 69 | 70 | Assert.Equivalent(new float[] { 1, 2, 3 }, serverVector); 71 | 72 | AssertSql( 73 | """ 74 | SELECT TOP(2) [p].[Embedding] 75 | FROM [Products] AS [p] 76 | WHERE [p].[Id] = 1 77 | """); 78 | } 79 | 80 | public class VectorSearchQueryFixture : SharedStoreFixtureBase 81 | { 82 | protected override string StoreName => "vectordb"; 83 | 84 | public TestSqlLoggerFactory TestSqlLoggerFactory 85 | => (TestSqlLoggerFactory)ListLoggerFactory; 86 | 87 | protected override ITestStoreFactory TestStoreFactory 88 | => SqlServerTestStoreFactory.Instance; 89 | 90 | protected override async Task SeedAsync(VectorSearchContext context) 91 | => await VectorSearchContext.SeedAsync(context); 92 | } 93 | 94 | public class VectorSearchContext(DbContextOptions options) : PoolableDbContext(options) 95 | { 96 | public DbSet Products { get; set; } 97 | 98 | public async static Task SeedAsync(VectorSearchContext context) 99 | { 100 | context.Products.AddRange( 101 | new Product { Id = 1, Embedding = [1, 2, 3] }, 102 | new Product { Id = 2, Embedding = [10, 20, 30] }); 103 | await context.SaveChangesAsync(); 104 | } 105 | } 106 | 107 | public class Product 108 | { 109 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 110 | public int Id { get; set; } 111 | 112 | [Column(TypeName = "vector(3)")] 113 | public float[] Embedding { get; set; } 114 | } 115 | 116 | protected VectorSearchContext CreateContext() 117 | => Fixture.CreateContext(); 118 | 119 | private void AssertSql(params string[] expected) 120 | => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); 121 | } 122 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.SqlServer.VectorSearch", "EFCore.SqlServer.VectorSearch\EFCore.SqlServer.VectorSearch.csproj", "{F30851D2-D4F3-4812-8422-B0F560291324}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.SqlServer.VectorSearch.Test", "EFCore.SqlServer.VectorSearch.Test\EFCore.SqlServer.VectorSearch.Test.csproj", "{A5611543-D6C3-443A-820B-5F5A1A157C30}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {F30851D2-D4F3-4812-8422-B0F560291324}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {F30851D2-D4F3-4812-8422-B0F560291324}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {F30851D2-D4F3-4812-8422-B0F560291324}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {F30851D2-D4F3-4812-8422-B0F560291324}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {A5611543-D6C3-443A-820B-5F5A1A157C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {A5611543-D6C3-443A-820B-5F5A1A157C30}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {A5611543-D6C3-443A-820B-5F5A1A157C30}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {A5611543-D6C3-443A-820B-5F5A1A157C30}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/EFCore.SqlServer.VectorSearch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 9.0.0-preview.3 5 | net8.0 6 | enable 7 | enable 8 | README.md 9 | MIT 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Extensions/SqlServerVectorSearchDbContextOptionsBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using EFCore.SqlServer.VectorSearch.Infrastructure.Internal; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace Microsoft.EntityFrameworkCore; 6 | 7 | /// 8 | /// Extension method for enabling Azure SQL vector search via . 9 | /// 10 | public static class SqlServerVectorSearchDbContextOptionsBuilderExtensions 11 | { 12 | /// 13 | /// Adds Azure SQL vector search functionality to Entity Framework Core. 14 | /// 15 | /// The options builder so that further configuration can be chained. 16 | public static AzureSqlDbContextOptionsBuilder UseVectorSearch(this AzureSqlDbContextOptionsBuilder optionsBuilder) 17 | { 18 | AddVectorSearchOptionsExtensions(optionsBuilder); 19 | 20 | return optionsBuilder; 21 | } 22 | 23 | /// 24 | /// Adds SQL Server vector search functionality to Entity Framework Core. 25 | /// 26 | /// The options builder so that further configuration can be chained. 27 | public static SqlServerDbContextOptionsBuilder UseVectorSearch(this SqlServerDbContextOptionsBuilder optionsBuilder) 28 | { 29 | AddVectorSearchOptionsExtensions(optionsBuilder); 30 | 31 | return optionsBuilder; 32 | } 33 | 34 | private static void AddVectorSearchOptionsExtensions(this IRelationalDbContextOptionsBuilderInfrastructure optionsBuilder) 35 | { 36 | var coreOptionsBuilder = optionsBuilder.OptionsBuilder; 37 | 38 | var extension = coreOptionsBuilder.Options.FindExtension() 39 | ?? new SqlServerVectorSearchOptionsExtension(); 40 | 41 | ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Extensions/SqlServerVectorSearchDbFunctionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Diagnostics; 2 | using Microsoft.EntityFrameworkCore.Query; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace Microsoft.EntityFrameworkCore; 6 | 7 | public static class SqlServerVectorSearchDbFunctionsExtensions 8 | { 9 | /// 10 | /// Returns the distance between two vectors, given a similarity measure. 11 | /// 12 | /// The instance. 13 | /// 14 | /// The similarity measure to use; can be dot, cosine, or euclidean. 15 | /// 16 | /// The first vector. 17 | /// The second vector. 18 | public static double VectorDistance( 19 | this DbFunctions _, 20 | [NotParameterized] string similarityMeasure, 21 | float[] vector1, 22 | float[] vector2) 23 | => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); 24 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Extensions/SqlServerVectorSearchPropertyBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using EFCore.SqlServer.VectorSearch.Storage.Internal; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace Microsoft.EntityFrameworkCore; 6 | 7 | public static class SqlServerVectorSearchPropertyBuilderExtensions 8 | { 9 | [Obsolete("Configure your property to use the 'vector' via HasColumnType()")] 10 | public static PropertyBuilder IsVector(this PropertyBuilder propertyBuilder) 11 | => throw new NotSupportedException(); 12 | 13 | [Obsolete("Configure your property to use the 'vector' via HasColumnType()")] 14 | public static PropertyBuilder IsVector(this PropertyBuilder propertyBuilder) 15 | => throw new NotSupportedException(); 16 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Extensions/SqlServerVectorSearchServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using EFCore.SqlServer.VectorSearch.Query.Internal; 2 | using EFCore.SqlServer.VectorSearch.Storage.Internal; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Query; 5 | using Microsoft.EntityFrameworkCore.Storage; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace Microsoft.Extensions.DependencyInjection; 9 | 10 | public static class SqlServerVectorSearchServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds the services required for Azure SQL vector search support for Entity Framework. 14 | /// 15 | /// The to add services to. 16 | /// The same service collection so that multiple calls can be chained. 17 | public static IServiceCollection AddEntityFrameworkSqlServerVectorSearch( 18 | this IServiceCollection serviceCollection) 19 | { 20 | new EntityFrameworkRelationalServicesBuilder(serviceCollection) 21 | .TryAdd() 22 | .TryAdd() 23 | .TryAdd(); 24 | 25 | return serviceCollection; 26 | } 27 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Infrastructure/SqlServerVectorSearchOptionsExtension.cs: -------------------------------------------------------------------------------- 1 | using EFCore.SqlServer.VectorSearch.Query.Internal; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Query; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace EFCore.SqlServer.VectorSearch.Infrastructure.Internal; 9 | 10 | /// 11 | /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 12 | /// the same compatibility standards as public APIs. It may be changed or removed without notice in 13 | /// any release. You should only use it directly in your code with extreme caution and knowing that 14 | /// doing so can result in application failures when updating to a new Entity Framework Core release. 15 | /// 16 | public class SqlServerVectorSearchOptionsExtension : IDbContextOptionsExtension 17 | { 18 | private DbContextOptionsExtensionInfo? _info; 19 | 20 | /// 21 | /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 22 | /// the same compatibility standards as public APIs. It may be changed or removed without notice in 23 | /// any release. You should only use it directly in your code with extreme caution and knowing that 24 | /// doing so can result in application failures when updating to a new Entity Framework Core release. 25 | /// 26 | public virtual void ApplyServices(IServiceCollection services) 27 | => services.AddEntityFrameworkSqlServerVectorSearch(); 28 | 29 | /// 30 | /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 31 | /// the same compatibility standards as public APIs. It may be changed or removed without notice in 32 | /// any release. You should only use it directly in your code with extreme caution and knowing that 33 | /// doing so can result in application failures when updating to a new Entity Framework Core release. 34 | /// 35 | public virtual DbContextOptionsExtensionInfo Info 36 | => _info ??= new ExtensionInfo(this); 37 | 38 | /// 39 | /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 40 | /// the same compatibility standards as public APIs. It may be changed or removed without notice in 41 | /// any release. You should only use it directly in your code with extreme caution and knowing that 42 | /// doing so can result in application failures when updating to a new Entity Framework Core release. 43 | /// 44 | public virtual void Validate(IDbContextOptions options) 45 | { 46 | var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; 47 | if (internalServiceProvider is not null) 48 | { 49 | using var scope = internalServiceProvider.CreateScope(); 50 | 51 | if (scope.ServiceProvider.GetService>() 52 | ?.Any(s => s is SqlServerVectorSearchMethodCallTranslatorPlugin) 53 | != true) 54 | { 55 | throw new InvalidOperationException( 56 | $"{nameof(SqlServerVectorSearchDbContextOptionsBuilderExtensions.UseVectorSearch)} requires {nameof(SqlServerVectorSearchServiceCollectionExtensions.AddEntityFrameworkSqlServerVectorSearch)} to be called on the internal service provider used."); 57 | } 58 | } 59 | } 60 | 61 | private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) 62 | { 63 | private new SqlServerVectorSearchOptionsExtension Extension 64 | => (SqlServerVectorSearchOptionsExtension)base.Extension; 65 | 66 | public override bool IsDatabaseProvider 67 | => false; 68 | 69 | public override int GetServiceProviderHashCode() 70 | => 0; 71 | 72 | public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) 73 | => true; 74 | 75 | public override void PopulateDebugInfo(IDictionary debugInfo) 76 | => debugInfo["SqlServer:" + nameof(SqlServerVectorSearchDbContextOptionsBuilderExtensions.UseVectorSearch)] = "1"; 77 | 78 | public override string LogFragment 79 | => "using SQL Server vector search"; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Query/SqlServerVectorSearchEvaluatableExpressionFilterPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Query; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace EFCore.SqlServer.VectorSearch.Query.Internal; 7 | 8 | public class SqlServerVectorSearchEvaluatableExpressionFilterPlugin : IEvaluatableExpressionFilterPlugin 9 | { 10 | public bool IsEvaluatableExpression(Expression expression) 11 | => expression switch 12 | { 13 | MethodCallExpression methodCallExpression 14 | when methodCallExpression.Method.DeclaringType == typeof(SqlServerVectorSearchDbFunctionsExtensions) 15 | => false, 16 | 17 | _ => true 18 | }; 19 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Query/SqlServerVectorSearchMethodCallTranslator.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using EFCore.SqlServer.VectorSearch.Storage.Internal; 3 | using Microsoft.Data.SqlClient; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Diagnostics; 6 | using Microsoft.EntityFrameworkCore.Query; 7 | using Microsoft.EntityFrameworkCore.Query.SqlExpressions; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | 10 | // ReSharper disable once CheckNamespace 11 | namespace EFCore.SqlServer.VectorSearch.Query.Internal; 12 | 13 | public class SqlServerVectorSearchMethodCallTranslator : IMethodCallTranslator 14 | { 15 | private readonly IRelationalTypeMappingSource _typeMappingSource; 16 | private readonly ISqlExpressionFactory _sqlExpressionFactory; 17 | // private readonly RelationalTypeMapping _vectorTypeMapping; 18 | 19 | public SqlServerVectorSearchMethodCallTranslator( 20 | IRelationalTypeMappingSource typeMappingSource, 21 | ISqlExpressionFactory sqlExpressionFactory) 22 | { 23 | _typeMappingSource = typeMappingSource; 24 | _sqlExpressionFactory = sqlExpressionFactory; 25 | } 26 | 27 | public SqlExpression? Translate( 28 | SqlExpression? instance, 29 | MethodInfo method, 30 | IReadOnlyList arguments, 31 | IDiagnosticsLogger logger) 32 | { 33 | if (method.DeclaringType != typeof(SqlServerVectorSearchDbFunctionsExtensions)) 34 | { 35 | return null; 36 | } 37 | 38 | switch (method.Name) 39 | { 40 | case nameof(SqlServerVectorSearchDbFunctionsExtensions.VectorDistance): 41 | if (arguments[1] is not SqlConstantExpression { Value: string similarityMeasure }) 42 | { 43 | throw new InvalidOperationException( 44 | "The first argument to EF.Functions.VectorDistance must be a constant string"); 45 | } 46 | 47 | // At least one of the two arguments must be a vector (i.e. SqlServerVectorTypeMapping). 48 | // Check this and extract the mapping, applying it to the other argument (e.g. in case it's a parameter). 49 | var vectorMapping = 50 | arguments[2].TypeMapping as SqlServerVectorTypeMapping 51 | ?? (arguments[3].TypeMapping as SqlServerVectorTypeMapping 52 | ?? throw new InvalidOperationException( 53 | "At least one of the arguments to EF.Functions.VectorDistance must be a vector")); 54 | 55 | var vector1 = Wrap(_sqlExpressionFactory.ApplyTypeMapping(arguments[2], vectorMapping), vectorMapping); 56 | var vector2 = Wrap(_sqlExpressionFactory.ApplyTypeMapping(arguments[3], vectorMapping), vectorMapping); 57 | 58 | SqlExpression Wrap(SqlExpression expression, SqlServerVectorTypeMapping vectorMapping) 59 | => expression is SqlParameterExpression 60 | ? _sqlExpressionFactory.Convert(expression, typeof(float[]), vectorMapping) 61 | : expression; 62 | 63 | return _sqlExpressionFactory.Function( 64 | "VECTOR_DISTANCE", 65 | [ 66 | _sqlExpressionFactory.Constant(similarityMeasure, _typeMappingSource.FindMapping("varchar(max)")), 67 | vector1, 68 | vector2 69 | ], 70 | nullable: true, 71 | [true, true, true], 72 | typeof(double), 73 | _typeMappingSource.FindMapping(typeof(double))); 74 | 75 | default: 76 | return null; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Query/SqlServerVectorSearchMethodCallTranslatorPlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Query; 2 | using Microsoft.EntityFrameworkCore.Storage; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace EFCore.SqlServer.VectorSearch.Query.Internal; 6 | 7 | public class SqlServerVectorSearchMethodCallTranslatorPlugin( 8 | IRelationalTypeMappingSource typeMappingSource, 9 | ISqlExpressionFactory sqlExpressionFactory) 10 | : IMethodCallTranslatorPlugin 11 | { 12 | /// 13 | /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 14 | /// the same compatibility standards as public APIs. It may be changed or removed without notice in 15 | /// any release. You should only use it directly in your code with extreme caution and knowing that 16 | /// doing so can result in application failures when updating to a new Entity Framework Core release. 17 | /// 18 | public virtual IEnumerable Translators { get; } 19 | = [new SqlServerVectorSearchMethodCallTranslator(typeMappingSource, sqlExpressionFactory)]; 20 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Storage/SqlServerVectorSearchTypeMappingSourcePlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Storage; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace EFCore.SqlServer.VectorSearch.Storage.Internal; 5 | 6 | public class SqlServerVectorSearchTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin 7 | { 8 | public RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) 9 | => mappingInfo.StoreTypeNameBase is "vector" 10 | ? new SqlServerVectorTypeMapping( 11 | mappingInfo.Size ?? throw new ArgumentException("'vector' store type must have a dimensions facet")) 12 | : null; 13 | } -------------------------------------------------------------------------------- /EFCore.SqlServer.VectorSearch/Storage/SqlServerVectorTypeMapping.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Text.Json; 3 | using Microsoft.EntityFrameworkCore.ChangeTracking; 4 | using Microsoft.EntityFrameworkCore.Storage; 5 | using Microsoft.EntityFrameworkCore.Storage.Json; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | // ReSharper disable once CheckNamespace 9 | namespace EFCore.SqlServer.VectorSearch.Storage.Internal; 10 | 11 | public class SqlServerVectorTypeMapping : RelationalTypeMapping 12 | { 13 | public SqlServerVectorTypeMapping(int dimensions) 14 | : this( 15 | new RelationalTypeMappingParameters( 16 | new CoreTypeMappingParameters(typeof(float[]), ConverterInstance, ComparerInstance, jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance /* TODO */), 17 | "vector", 18 | StoreTypePostfix.Size, 19 | System.Data.DbType.String, 20 | size: dimensions)) 21 | { 22 | } 23 | 24 | protected SqlServerVectorTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) 25 | { 26 | } 27 | 28 | /// 29 | /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 30 | /// the same compatibility standards as public APIs. It may be changed or removed without notice in 31 | /// any release. You should only use it directly in your code with extreme caution and knowing that 32 | /// doing so can result in application failures when updating to a new Entity Framework Core release. 33 | /// 34 | protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) 35 | => new SqlServerVectorTypeMapping(parameters); 36 | 37 | /// 38 | /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 39 | /// the same compatibility standards as public APIs. It may be changed or removed without notice in 40 | /// any release. You should only use it directly in your code with extreme caution and knowing that 41 | /// doing so can result in application failures when updating to a new Entity Framework Core release. 42 | /// 43 | // Note: we assume that the string argument has gone through the value converter below, and so doesn't need 44 | // escaping. 45 | protected override string GenerateNonNullSqlLiteral(object value) 46 | => $"CAST('{(string)value}' AS vector({Size}))"; 47 | 48 | private static readonly VectorComparer ComparerInstance = new(); 49 | private static readonly VectorConverter ConverterInstance = new(); 50 | 51 | private class VectorComparer() : ValueComparer( 52 | (x, y) => x == null ? y == null : y != null && x.SequenceEqual(y), 53 | v => v.Aggregate(0, (a, b) => HashCode.Combine(a, b.GetHashCode())), 54 | v => v.ToArray()); 55 | 56 | private class VectorConverter() : ValueConverter( 57 | f => JsonSerializer.Serialize(f, JsonSerializerOptions.Default), 58 | s => JsonSerializer.Deserialize(s, JsonSerializerOptions.Default)!); 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Shay Rojansky 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EFCore.SqlServer.VectorSearch 2 | 3 | > [!IMPORTANT] 4 | > This plugin is in prerelease status, and the APIs described below are likely to change before the final release. 5 | > Vector Functions are in Public Preview. Learn the details about vectors in Azure SQL here: https://aka.ms/azure-sql-vector-public-preview 6 | 7 | This Entity Framework Core plugin provides integration between EF and Vector Support in Azure SQL Database, allowing LINQ to be used to perform vector similarity search, and seamless insertion/retrieval of vector data. 8 | 9 | To use the plugin, reference the [EFCore.SqlServer.VectorSearch](https://www.nuget.org/packages/EFCore.SqlServer.VectorSearch) nuget package, and enable the plugin by adding `UseVectorSearch()` to your `UseSqlServer()` or `UseAzureSql()` config as follows: 10 | 11 | ```c# 12 | builder.Services.AddDbContext(options => 13 | options.UseSqlServer("", o => o.UseVectorSearch())); 14 | ``` 15 | 16 | Once the plugin has been enabled, add an ordinary `float[]` property to the .NET type being mapped with EF: 17 | 18 | ```c# 19 | public class Product 20 | { 21 | public int Id { get; set; } 22 | public float[] Embedding { get; set; } 23 | } 24 | ``` 25 | 26 | Finally, configure the property to be mapped as a vector by letting EF Core know using the `HasColumnType` method. Use the `vector` type and specify the number of dimension that your vector will have: 27 | 28 | ```c# 29 | protected override void OnModelCreating(ModelBuilder modelBuilder) 30 | { 31 | modelBuilder.Entity().Property(p => p.Embedding).HasColumnType("vector(3)"); 32 | } 33 | ``` 34 | 35 | That's it - you can now perform similarity search in LINQ queries! For example, to get the top 5 most similar products: 36 | 37 | ```c# 38 | var someVector = new[] { 1f, 2f, 3f }; 39 | var products = await context.Products 40 | .OrderBy(p => EF.Functions.VectorDistance("cosine", p.Embedding, someVector)) 41 | .Take(5) 42 | .ToArrayAsync(); 43 | ``` 44 | 45 | A full sample using EF Core and vectors is available here: 46 | 47 | [Azure SQL DB Vector Samples - EF-Core Sample](https://github.com/Azure-Samples/azure-sql-db-vector-search/tree/main/DotNet/EF-Core) 48 | 49 | Ideas? Issues? Let us know on the [issues page](https://github.com/efcore/EFCore.SqlServer.VectorSearch/issues). 50 | --------------------------------------------------------------------------------