├── .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