diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4bc5af2b..9e5be7d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,39 +1,41 @@ -name: CI - -on: - push - -env: - UseSqlServerContainer: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - - name: install .NET Core 8 SDK - uses: actions/setup-dotnet@v2 - with: - dotnet-version: 8.0.x - include-prerelease: false - - - name: checkout repository - uses: actions/checkout@v2 - - - name: dotnet restore - run: dotnet restore - - - name: build - run: dotnet build --configuration Release --no-restore - - - name: test - run: dotnet test --configuration Release --no-build - - - name: test results - uses: EnricoMi/publish-unit-test-result-action@v2 - id: test-results - if: always() - with: - check_name: tests results - trx_files: "**/test-results/**/*.trx" +name: CI + +on: + push + +env: + UseSqlServerContainer: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + + - name: install .NET Core 8/9 SDKs + uses: actions/setup-dotnet@v2 + with: + include-prerelease: false + dotnet-version: | + 8.0.x + 9.0.x + + - name: checkout repository + uses: actions/checkout@v2 + + - name: dotnet restore + run: dotnet restore + + - name: build + run: dotnet build --configuration Release --no-restore + + - name: test + run: dotnet test --configuration Release --no-build + + - name: test results + uses: EnricoMi/publish-unit-test-result-action@v2 + id: test-results + if: always() + with: + check_name: tests results + trx_files: "**/test-results/**/*.trx" diff --git a/Directory.Build.props b/Directory.Build.props index 8b82da23..b6a3d9c3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ (c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved. - 8.1.1 + 9.0.0 Pawel Gerr true https://dev.azure.com/pawelgerr/Thinktecture.EntityFrameworkCore @@ -13,7 +13,7 @@ false Thinktecture net8.0 - 12.0 + 13.0 enable $(NoWarn);CA1303;MSB3884; enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 3b62b687..5c022e3e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,14 +2,14 @@ - - - - - - - - + + + + + + + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index eaabf973..e867d98f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,6 +23,14 @@ steps: includePreviewVersions: false installationPath: $(Agent.ToolsDirectory)/dotnet +- task: UseDotNet@2 + displayName: 'use .NET 9.0 SDK' + inputs: + packageType: sdk + version: 9.0.x + includePreviewVersions: false + installationPath: $(Agent.ToolsDirectory)/dotnet + - script: | echo dotnet --version dotnet --version diff --git a/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionOrderingsExpression.cs b/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionOrderingsExpression.cs index 7cc2b0ca..939c257d 100644 --- a/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionOrderingsExpression.cs +++ b/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionOrderingsExpression.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using System.Reflection; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -10,6 +11,8 @@ namespace Thinktecture.EntityFrameworkCore.Query.SqlExpressions; /// public sealed class WindowFunctionOrderingsExpression : SqlExpression, INotNullableSqlExpression { + private static readonly ConstructorInfo _quotingConstructor = typeof(WindowFunctionOrderingsExpression).GetConstructors().Single(); + /// /// Orderings. /// @@ -39,6 +42,13 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) return ReferenceEquals(visited, Orderings) ? this : new WindowFunctionOrderingsExpression(visited); } + /// + public override Expression Quote() + { + return New(_quotingConstructor, + NewArrayInit(typeof(OrderingExpression), initializers: Orderings.Select(o => o.Quote()))); + } + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionPartitionByExpression.cs b/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionPartitionByExpression.cs index 5b195344..dc351d40 100644 --- a/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionPartitionByExpression.cs +++ b/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionPartitionByExpression.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using System.Reflection; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -10,6 +11,8 @@ namespace Thinktecture.EntityFrameworkCore.Query.SqlExpressions; /// public class WindowFunctionPartitionByExpression : SqlExpression, INotNullableSqlExpression { + private static readonly ConstructorInfo _quotingConstructor = typeof(WindowFunctionPartitionByExpression).GetConstructors().Single(); + /// /// Partition by expressions. /// @@ -39,6 +42,13 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) return ReferenceEquals(visited, PartitionBy) ? this : new WindowFunctionPartitionByExpression(visited); } + /// + public override Expression Quote() + { + return New(_quotingConstructor, + NewArrayInit(typeof(SqlExpression), initializers: PartitionBy.Select(o => o.Quote()))); + } + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/ThinktectureSqlNullabilityProcessor.cs b/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/ThinktectureSqlNullabilityProcessor.cs index 42547402..82507666 100644 --- a/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/ThinktectureSqlNullabilityProcessor.cs +++ b/src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/ThinktectureSqlNullabilityProcessor.cs @@ -12,8 +12,8 @@ public class ThinktectureSqlNullabilityProcessor : SqlNullabilityProcessor /// public ThinktectureSqlNullabilityProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } diff --git a/src/Thinktecture.EntityFrameworkCore.Relational/Extensions/RelationalQueryableMethodTranslatingExpressionVisitorExtensions.cs b/src/Thinktecture.EntityFrameworkCore.Relational/Extensions/RelationalQueryableMethodTranslatingExpressionVisitorExtensions.cs index d0907af5..c864e7e7 100644 --- a/src/Thinktecture.EntityFrameworkCore.Relational/Extensions/RelationalQueryableMethodTranslatingExpressionVisitorExtensions.cs +++ b/src/Thinktecture.EntityFrameworkCore.Relational/Extensions/RelationalQueryableMethodTranslatingExpressionVisitorExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Thinktecture.EntityFrameworkCore; using Thinktecture.EntityFrameworkCore.Internal; using Thinktecture.Internal; @@ -54,8 +55,13 @@ private static Expression TranslateTableHints( ShapedQueryExpression shapedQueryExpression, MethodCallExpression methodCallExpression) { - var tableHintsExpression = (TableHintsExpression)methodCallExpression.Arguments[1] ?? throw new InvalidOperationException("Table hints cannot be null."); - var tableHints = tableHintsExpression.Value ?? throw new Exception("No table hints provided."); + var hintArgs = methodCallExpression.Arguments[1]; + var tableHints = hintArgs switch + { + TableHintsExpression tableHintsExpression => tableHintsExpression.Value, + ConstantExpression constantExpression => (IReadOnlyList?)constantExpression.Value ?? throw new Exception("No table hints provided."), + _ => throw new NotSupportedException($"Table hint argument of type '{hintArgs.GetType().FullName}' is not supported.") + }; if (tableHints.Count == 0) return shapedQueryExpression; diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProvider.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProvider.cs index e48adf6f..7e2498b5 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProvider.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProvider.cs @@ -239,76 +239,6 @@ protected virtual IDbContextTransaction BeginTransaction(T ctx) return ctx.Database.BeginTransaction(_sharedTablesIsolationLevel); } - /// - /// Starts a new transaction for migration and cleanup. - /// - /// Database context. - /// An instance of . - protected virtual IDbContextTransaction? BeginMigrationAndCleanupTransaction(T ctx) - { - ArgumentNullException.ThrowIfNull(ctx); - - if (!_lockTableEnabled) - return ctx.Database.BeginTransaction(IsolationLevel.Serializable); - - var sqlGenerationHelper = ctx.GetService(); - var lockTableName = sqlGenerationHelper.DelimitIdentifier(_lockTableName, _lockTableSchema); - - CreateTestIsolationTable(ctx, lockTableName); - - for (var i = 0;; i++) - { - var tx = ctx.Database.BeginTransaction(IsolationLevel.Serializable); - - try - { - LockDatabase(ctx, lockTableName); - return tx; - } - catch (Exception) - { - tx.Dispose(); - - if (i > _maxNumberOfLockRetries) - throw; - - var delay = new TimeSpan(_random.NextInt64(_minRetryDelay.Ticks, _maxRetryDelay.Ticks)); - Task.Delay(delay).GetAwaiter().GetResult(); - } - } - } - - private void CreateTestIsolationTable(T ctx, string lockTableName) - { - var createTableSql = $""" - IF(OBJECT_ID('{lockTableName}') IS NULL) - CREATE TABLE {lockTableName}(Id INT NOT NULL) - """; - - for (var i = 0;; i++) - { - try - { - ctx.Database.ExecuteSqlRaw(createTableSql); - return; - } - catch (Exception) - { - if (i > _maxNumberOfLockRetries) - throw; - - var delay = new TimeSpan(_random.NextInt64(_minRetryDelay.Ticks, _maxRetryDelay.Ticks)); - Task.Delay(delay).GetAwaiter().GetResult(); - } - } - } - - private static void LockDatabase(T ctx, string lockTableName) - { - var selectSql = $"SELECT * FROM {lockTableName} WITH (HOLDLOCK, UPDLOCK)"; - ctx.Database.ExecuteSqlRaw(selectSql); - } - /// /// Runs migrations for provided . /// @@ -326,21 +256,7 @@ protected virtual void RunMigrations(T ctx) try { LogLevelSwitch.MinimumLogLevel = _testingLoggingOptions.MigrationLogLevel; - - IDbContextTransaction? migrationTx = null; - - if (ctx.Database.CurrentTransaction is null) - migrationTx = BeginMigrationAndCleanupTransaction(ctx); - - try - { - _migrationExecutionStrategy.Migrate(ctx); - migrationTx?.Commit(); - } - finally - { - migrationTx?.Dispose(); - } + _migrationExecutionStrategy.Migrate(ctx); } finally { @@ -427,22 +343,7 @@ private async Task DisposeContextsAndRollbackMigrationsAsync(CancellationToken c { // Create a new ctx as a last resort to rollback migrations and clean up the database await using var ctx = _actDbContext ?? _arrangeDbContext ?? _assertDbContext ?? CreateDbContext(_masterDbContextOptions, Schema is null ? null : new DbDefaultSchema(Schema)); - - IDbContextTransaction? migrationTx = null; - - if (ctx.Database.CurrentTransaction is null) - migrationTx = BeginMigrationAndCleanupTransaction(ctx); - - try - { - await _isolationOptions.CleanupAsync(ctx, Schema, cancellationToken); - - await (migrationTx?.CommitAsync(cancellationToken) ?? Task.CompletedTask); - } - finally - { - await (migrationTx?.DisposeAsync() ?? ValueTask.CompletedTask); - } + await _isolationOptions.CleanupAsync(ctx, Schema, cancellationToken); } await (_arrangeDbContext?.DisposeAsync() ?? ValueTask.CompletedTask); diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionExpression.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionExpression.cs index f9e2e0da..b95b69e1 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionExpression.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/SqlExpressions/WindowFunctionExpression.cs @@ -1,7 +1,8 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; +using System.Reflection; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; using Microsoft.EntityFrameworkCore.Storage; namespace Thinktecture.EntityFrameworkCore.Query.SqlExpressions; @@ -11,6 +12,8 @@ namespace Thinktecture.EntityFrameworkCore.Query.SqlExpressions; /// public class WindowFunctionExpression : SqlExpression { + private static readonly ConstructorInfo _quotingConstructor = typeof(WindowFunctionExpression).GetConstructors().Single(); + /// /// Creates a new instance of the class. /// @@ -75,6 +78,20 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) : this; } + /// + [Experimental("EF9100")] + public override Expression Quote() + { + return New(_quotingConstructor, + Constant(Name), + Constant(UseAsteriskWhenNoArguments), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping), + NewArrayInit(typeof(SqlExpression), Arguments.Select(a => a.Quote())), + NewArrayInit(typeof(SqlExpression), Partitions.Select(p => p.Quote())), + NewArrayInit(typeof(OrderingExpression), Orderings.Select(o => o.Quote()))); + } + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessor.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessor.cs index 5b6c4818..a3f56a21 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessor.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessor.cs @@ -12,8 +12,8 @@ public class ThinktectureSqlServerParameterBasedSqlProcessor : SqlServerParamete /// public ThinktectureSqlServerParameterBasedSqlProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } @@ -23,6 +23,6 @@ protected override Expression ProcessSqlNullability(Expression selectExpression, ArgumentNullException.ThrowIfNull(selectExpression); ArgumentNullException.ThrowIfNull(parametersValues); - return new ThinktectureSqlServerSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(selectExpression, parametersValues, out canCache); + return new ThinktectureSqlServerSqlNullabilityProcessor(Dependencies, Parameters).Process(selectExpression, parametersValues, out canCache); } } diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessorFactory.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessorFactory.cs index 811899ee..e9ca5de5 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessorFactory.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerParameterBasedSqlProcessorFactory.cs @@ -18,8 +18,8 @@ public ThinktectureSqlServerParameterBasedSqlProcessorFactory( } /// - public RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) + public RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) { - return new ThinktectureSqlServerParameterBasedSqlProcessor(_dependencies, useRelationalNulls); + return new ThinktectureSqlServerParameterBasedSqlProcessor(_dependencies, parameters); } } diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerSqlNullabilityProcessor.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerSqlNullabilityProcessor.cs index d2851535..ca980d76 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerSqlNullabilityProcessor.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Query/ThinktectureSqlServerSqlNullabilityProcessor.cs @@ -13,8 +13,10 @@ namespace Thinktecture.EntityFrameworkCore.Query; public class ThinktectureSqlServerSqlNullabilityProcessor : SqlServerSqlNullabilityProcessor { /// - public ThinktectureSqlServerSqlNullabilityProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + public ThinktectureSqlServerSqlNullabilityProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/TempTables/SqlServerTempTableReference.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/TempTables/SqlServerTempTableReference.cs index 0f494777..3fd845a1 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/TempTables/SqlServerTempTableReference.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/TempTables/SqlServerTempTableReference.cs @@ -1,4 +1,6 @@ using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -56,15 +58,13 @@ public void Dispose() try { - if (!_dropTableOnDispose || _database.GetDbConnection().State != ConnectionState.Open) + using var command = TryCreateCleanupCommand(); + + if (command is null) return; - var sql = $""" - IF(OBJECT_ID('tempdb..{Name}') IS NOT NULL) - DROP TABLE {_sqlGenerationHelper.DelimitIdentifier(Name)}; - """; + command.ExecuteNonQuery(); - _database.ExecuteSqlRaw(sql); _database.CloseConnection(); } catch (ObjectDisposedException ex) @@ -87,14 +87,13 @@ public async ValueTask DisposeAsync() try { - if (!_dropTableOnDispose || _database.GetDbConnection().State != ConnectionState.Open) + await using var command = TryCreateCleanupCommand(); + + if (command is null) return; - var sql = $""" - IF(OBJECT_ID('tempdb..{Name}') IS NOT NULL) - DROP TABLE {_sqlGenerationHelper.DelimitIdentifier(Name)}; - """; - await _database.ExecuteSqlRawAsync(sql).ConfigureAwait(false); + await command.ExecuteNonQueryAsync(); + await _database.CloseConnectionAsync().ConfigureAwait(false); } catch (ObjectDisposedException ex) @@ -106,4 +105,32 @@ public async ValueTask DisposeAsync() _nameLease.Dispose(); } } + + private DbCommand? TryCreateCleanupCommand() + { + DbCommand? command = null; + + try + { + if (!_dropTableOnDispose) + return null; + + var connection = _database.GetDbConnection(); + + if (connection.State != ConnectionState.Open) + return null; + + command = connection.CreateCommand(); + command.CommandText = $""" + IF(OBJECT_ID('tempdb..{Name}') IS NOT NULL) + DROP TABLE {_sqlGenerationHelper.DelimitIdentifier(Name)}; + """; + return command; + } + catch + { + command?.Dispose(); + throw; + } + } } diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs deleted file mode 100644 index 00a241c6..00000000 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Thinktecture.EntityFrameworkCore; - -namespace Thinktecture; - -/// -/// Extension methods for . -/// -public static class SqlServerQueryableExtensions -{ - /// - /// Adds table hints to a table specified in . - /// - /// Query using a table to apply table hints to. - /// Table hints. - /// Entity type. - /// Query with table hints applied. - public static IQueryable WithTableHints(this IQueryable source, params SqlServerTableHint[] hints) - { - return source.WithTableHints((IReadOnlyList)hints); - } -} diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessor.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessor.cs index 90bdd79f..f65cd82c 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessor.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessor.cs @@ -14,8 +14,8 @@ public class ThinktectureSqliteParameterBasedSqlProcessor : SqliteParameterBased /// public ThinktectureSqliteParameterBasedSqlProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } @@ -25,6 +25,6 @@ protected override Expression ProcessSqlNullability(Expression expression, IRead ArgumentNullException.ThrowIfNull(expression); ArgumentNullException.ThrowIfNull(parametersValues); - return new ThinktectureSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(expression, parametersValues, out canCache); + return new ThinktectureSqlNullabilityProcessor(Dependencies, Parameters).Process(expression, parametersValues, out canCache); } } diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessorFactory.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessorFactory.cs index dfc92827..409a723b 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessorFactory.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureRelationalParameterBasedSqlProcessorFactory.cs @@ -18,8 +18,8 @@ public ThinktectureSqliteParameterBasedSqlProcessorFactory( } /// - public RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) + public RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) { - return new ThinktectureSqliteParameterBasedSqlProcessor(_dependencies, useRelationalNulls); + return new ThinktectureSqliteParameterBasedSqlProcessor(_dependencies, parameters); } } diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor.cs index b1bae813..5dc4881a 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor.cs @@ -15,7 +15,7 @@ public class ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor : Sql public ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, - QueryCompilationContext queryCompilationContext) + RelationalQueryCompilationContext queryCompilationContext) : base(dependencies, relationalDependencies, queryCompilationContext) { } diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitorFactory.cs index 7a213eaa..2568e9be 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitorFactory.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/Query/ThinktectureSqliteQueryableMethodTranslatingExpressionVisitorFactory.cs @@ -26,6 +26,8 @@ public ThinktectureSqliteQueryableMethodTranslatingExpressionVisitorFactory( /// public QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) { - return new ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor(_dependencies, _relationalDependencies, queryCompilationContext); + return new ThinktectureSqliteQueryableMethodTranslatingExpressionVisitor(_dependencies, + _relationalDependencies, + (RelationalQueryCompilationContext)queryCompilationContext); } } diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/TempTables/SqliteTempTableReference.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/TempTables/SqliteTempTableReference.cs index 859da6ed..3abff51f 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/TempTables/SqliteTempTableReference.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/TempTables/SqliteTempTableReference.cs @@ -1,4 +1,6 @@ using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -49,12 +51,13 @@ public void Dispose() { try { - if (!_dropTableOnDispose || _database.GetDbConnection().State != ConnectionState.Open) + using var command = TryCreateCleanupCommand(); + + if (command is null) return; - var sql = $"DROP TABLE IF EXISTS {_sqlGenerationHelper.DelimitIdentifier(Name, "temp")}"; + command.ExecuteNonQuery(); - _database.ExecuteSqlRaw(sql); _database.CloseConnection(); } catch (ObjectDisposedException ex) @@ -72,12 +75,13 @@ public async ValueTask DisposeAsync() { try { - if (!_dropTableOnDispose || _database.GetDbConnection().State != ConnectionState.Open) + await using var command = TryCreateCleanupCommand(); + + if (command is null) return; - var sql = $"DROP TABLE IF EXISTS {_sqlGenerationHelper.DelimitIdentifier(Name, "temp")}"; + await command.ExecuteNonQueryAsync(); - await _database.ExecuteSqlRawAsync(sql).ConfigureAwait(false); await _database.CloseConnectionAsync().ConfigureAwait(false); } catch (ObjectDisposedException ex) @@ -89,4 +93,32 @@ public async ValueTask DisposeAsync() _nameLease.Dispose(); } } + + private DbCommand? TryCreateCleanupCommand() + { + DbCommand? command = null; + + try + { + if (!_dropTableOnDispose) + return null; + + var connection = _database.GetDbConnection(); + + if (connection.State != ConnectionState.Open) + return null; + + var sql = $"DROP TABLE IF EXISTS {_sqlGenerationHelper.DelimitIdentifier(Name, "temp")}"; + + command = connection.CreateCommand(); + command.CommandText = sql; + + return command; + } + catch + { + command?.Dispose(); + throw; + } + } } diff --git a/src/Thinktecture.EntityFrameworkCore.Testing/EntityFrameworkCore/IMigrationExecutionStrategy.cs b/src/Thinktecture.EntityFrameworkCore.Testing/EntityFrameworkCore/IMigrationExecutionStrategy.cs index eab0dda9..bf334a4f 100644 --- a/src/Thinktecture.EntityFrameworkCore.Testing/EntityFrameworkCore/IMigrationExecutionStrategy.cs +++ b/src/Thinktecture.EntityFrameworkCore.Testing/EntityFrameworkCore/IMigrationExecutionStrategy.cs @@ -13,7 +13,7 @@ public interface IMigrationExecutionStrategy public static readonly IMigrationExecutionStrategy NoMigration = new NoMigrationExecutionStrategy(); /// - /// The database will be migrated using . + /// The database will be migrated using . /// public static readonly IMigrationExecutionStrategy Migrations = new MigrationExecutionStrategy(); diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index ecdde475..1ab93600 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,28 +1,33 @@ - - $([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')) - false - $(NoWarn);CA1062;EF1002;xUnit1041 - + + $([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')) + false + $(NoWarn);CA1062;EF1002;xUnit1041 + - + - - - - - - - - - + + + net8.0;net9.0 + - - - - - - + + + + + + + + + + + + + + + + diff --git a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/Extensions/QueryableExtensionsTests/WithTableHints.cs b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/Extensions/QueryableExtensionsTests/WithTableHints.cs index 9fd12fae..7275d84f 100644 --- a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/Extensions/QueryableExtensionsTests/WithTableHints.cs +++ b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/Extensions/QueryableExtensionsTests/WithTableHints.cs @@ -121,14 +121,14 @@ public async Task Should_add_table_hints_to_table_and_owned_entities() { var query = ActDbContext.TestEntities_Own_SeparateMany_SeparateMany.WithTableHints(SqlServerTableHint.NoLock); - query.ToQueryString().Should().Be("SELECT [t].[Id], [t0].[TestEntity_Owns_SeparateMany_SeparateManyId], [t0].[Id], [t0].[IntColumn], [t0].[StringColumn], [t0].[OwnedEntity_Owns_SeparateManyTestEntity_Owns_SeparateMany_SeparateManyId], [t0].[OwnedEntity_Owns_SeparateManyId], [t0].[Id0], [t0].[IntColumn0], [t0].[StringColumn0]" + Environment.NewLine + + query.ToQueryString().Should().Be("SELECT [t].[Id], [s1].[TestEntity_Owns_SeparateMany_SeparateManyId], [s1].[Id], [s1].[IntColumn], [s1].[StringColumn], [s1].[OwnedEntity_Owns_SeparateManyTestEntity_Owns_SeparateMany_SeparateManyId], [s1].[OwnedEntity_Owns_SeparateManyId], [s1].[Id0], [s1].[IntColumn0], [s1].[StringColumn0]" + Environment.NewLine + $"FROM {EscapedSchema}.[TestEntities_Own_SeparateMany_SeparateMany] AS [t] WITH (NOLOCK)" + Environment.NewLine + "LEFT JOIN (" + Environment.NewLine + " SELECT [s].[TestEntity_Owns_SeparateMany_SeparateManyId], [s].[Id], [s].[IntColumn], [s].[StringColumn], [s0].[OwnedEntity_Owns_SeparateManyTestEntity_Owns_SeparateMany_SeparateManyId], [s0].[OwnedEntity_Owns_SeparateManyId], [s0].[Id] AS [Id0], [s0].[IntColumn] AS [IntColumn0], [s0].[StringColumn] AS [StringColumn0]" + Environment.NewLine + $" FROM {EscapedSchema}.[SeparateEntitiesMany_SeparateEntitiesMany] AS [s] WITH (NOLOCK)" + Environment.NewLine + $" LEFT JOIN {EscapedSchema}.[SeparateEntitiesMany_SeparateEntitiesMany_Inner] AS [s0] WITH (NOLOCK) ON [s].[TestEntity_Owns_SeparateMany_SeparateManyId] = [s0].[OwnedEntity_Owns_SeparateManyTestEntity_Owns_SeparateMany_SeparateManyId] AND [s].[Id] = [s0].[OwnedEntity_Owns_SeparateManyId]" + Environment.NewLine + - ") AS [t0] ON [t].[Id] = [t0].[TestEntity_Owns_SeparateMany_SeparateManyId]" + Environment.NewLine + - "ORDER BY [t].[Id], [t0].[TestEntity_Owns_SeparateMany_SeparateManyId], [t0].[Id], [t0].[OwnedEntity_Owns_SeparateManyTestEntity_Owns_SeparateMany_SeparateManyId], [t0].[OwnedEntity_Owns_SeparateManyId]"); + ") AS [s1] ON [t].[Id] = [s1].[TestEntity_Owns_SeparateMany_SeparateManyId]" + Environment.NewLine + + "ORDER BY [t].[Id], [s1].[TestEntity_Owns_SeparateMany_SeparateManyId], [s1].[Id], [s1].[OwnedEntity_Owns_SeparateManyTestEntity_Owns_SeparateMany_SeparateManyId], [s1].[OwnedEntity_Owns_SeparateManyId]"); var result = await query.ToListAsync(); result.Should().BeEmpty(); @@ -211,9 +211,9 @@ public async Task Should_work_with_projection_using_SplitQuery_behavior() " FROM [#TempTable_1] AS [#] WITH (UPDLOCK)" + Environment.NewLine + ")" + Environment.NewLine + "ORDER BY [t].[Id]"); - ExecutedCommands.Last().Should().Be("SELECT [t0].[Id], [t].[Id]" + Environment.NewLine + + ExecutedCommands.Last().Should().Be("SELECT [t2].[Id], [t].[Id]" + Environment.NewLine + $"FROM [{TestCtxProvider.Schema}].[TestEntities] AS [t] WITH (UPDLOCK, ROWLOCK)" + Environment.NewLine + - $"INNER JOIN [{TestCtxProvider.Schema}].[TestEntities] AS [t0] ON [t].[Id] = [t0].[ParentId]" + Environment.NewLine + + $"INNER JOIN [{TestCtxProvider.Schema}].[TestEntities] AS [t2] ON [t].[Id] = [t2].[ParentId]" + Environment.NewLine + "WHERE [t].[Id] IN (" + Environment.NewLine + " SELECT [#].[Column1]" + Environment.NewLine + " FROM [#TempTable_1] AS [#] WITH (UPDLOCK)" + Environment.NewLine + diff --git a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/IntegrationTestsBase.cs b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/IntegrationTestsBase.cs index d0762008..ef95748f 100644 --- a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/IntegrationTestsBase.cs +++ b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/IntegrationTestsBase.cs @@ -16,6 +16,7 @@ public class IntegrationTestsBase : SqlServerDbContextIntegrationTests? ConfigureModel { get; set; } + protected Action? Configure { get; set; } protected IReadOnlyCollection ExecutedCommands => TestCtxProvider.ExecutedCommands ?? throw new InvalidOperationException("Capturing executed commands wasn't enabled."); protected string? Schema => TestCtxProvider.Schema; @@ -25,6 +26,7 @@ public class IntegrationTestsBase : SqlServerDbContextIntegrationTests b.ConfigureWarnings(warningsBuilder => warningsBuilder.Ignore(RelationalEventId.PendingModelChangesWarning)); } private static bool NonExistingTableFilter(IEntityType entityType) @@ -76,6 +78,10 @@ protected override void ConfigureTestDbContextProvider(SqlServerTestDbContextPro optionsBuilder.AddTenantDatabaseSupport(); }) .UseSharedTableSchema(schema) - .InitializeContext(ctx => ctx.ConfigureModel = ConfigureModel); + .InitializeContext(ctx => + { + ctx.ConfigureModel = ConfigureModel; + ctx.Configure = Configure; + }); } } diff --git a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/TestDatabaseContext/TestDbContext.cs b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/TestDatabaseContext/TestDbContext.cs index d28e11a8..1c7168a3 100644 --- a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/TestDatabaseContext/TestDbContext.cs +++ b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/TestDatabaseContext/TestDbContext.cs @@ -40,6 +40,7 @@ public class TestDbContext : DbContext, IDbDefaultSchema #nullable enable public Action? ConfigureModel { get; set; } + public Action? Configure { get; set; } public TestDbContext(DbContextOptions options, IDbDefaultSchema? schema) : base(options) @@ -47,11 +48,18 @@ public TestDbContext(DbContextOptions options, IDbDefaultSchema? Schema = schema?.Schema; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + Configure?.Invoke(optionsBuilder); + } + /// protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder.Properties(builder => builder - .HavePrecision(18, 5)); + .HavePrecision(18, 5)); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertAsync.cs b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertAsync.cs index e72baf3e..07680a7c 100644 --- a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertAsync.cs +++ b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertAsync.cs @@ -1,4 +1,5 @@ using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Thinktecture.TestDatabaseContext; @@ -19,6 +20,7 @@ public BulkInsertAsync(ITestOutputHelper testOutputHelper) public async Task Should_throw_when_inserting_entities_without_creating_table_first() { ConfigureModel = builder => builder.Entity().HasNoKey(); + Configure = builder => builder.ConfigureWarnings(warningsBuilder => warningsBuilder.Ignore(RelationalEventId.PendingModelChangesWarning)); await SUT.Awaiting(sut => sut.BulkInsertAsync(new List(), new SqliteBulkInsertOptions())) .Should().ThrowAsync().WithMessage("Error during bulk operation on table '\"CustomTempTable\"'. See inner exception for more details."); diff --git a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/SchemaChangingIntegrationTestsBase.cs b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/SchemaChangingIntegrationTestsBase.cs index f6795a3f..188b70ad 100644 --- a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/SchemaChangingIntegrationTestsBase.cs +++ b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/SchemaChangingIntegrationTestsBase.cs @@ -10,6 +10,7 @@ public abstract class SchemaChangingIntegrationTestsBase : SqliteDbContextIntegr private readonly IMigrationExecutionStrategy? _migrationExecutionStrategy; protected Action? ConfigureModel { get; set; } + protected Action? Configure { get; set; } protected ILoggerFactory LoggerFactory { get; } protected SchemaChangingIntegrationTestsBase( @@ -27,7 +28,11 @@ protected override void ConfigureTestDbContextProvider(SqliteTestDbContextProvid .UseMigrationLogLevel(LogLevel.Warning) .ConfigureSqliteOptions(optionsBuilder => optionsBuilder.AddBulkOperationSupport() .AddWindowFunctionsSupport()) - .InitializeContext(ctx => ctx.ConfigureModel = ConfigureModel); + .InitializeContext(ctx => + { + ctx.ConfigureModel = ConfigureModel; + ctx.Configure = Configure; + }); if (ConfigureModel is not null) builder.DisableModelCache(); diff --git a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/TestDatabaseContext/TestDbContext.cs b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/TestDatabaseContext/TestDbContext.cs index 3a06c5d5..4dec2584 100644 --- a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/TestDatabaseContext/TestDbContext.cs +++ b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/TestDatabaseContext/TestDbContext.cs @@ -31,12 +31,20 @@ public class TestDbContext : DbContext #nullable enable public Action? ConfigureModel { get; set; } + public Action? Configure { get; set; } public TestDbContext(DbContextOptions options) : base(options) { } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + Configure?.Invoke(optionsBuilder); + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder);