diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index aeaa1faffb..ce45d1fc5e 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1141,6 +1141,10 @@ internal async ValueTask ExecuteReader(CommandBehavior behavio { if (conn.TryGetBoundConnector(out var connector)) { + cancellationToken.ThrowIfCancellationRequested(); + // We cannot pass a token here, as we'll cancel a non-send query + // Also, we don't pass the cancellation token to StartUserAction, since that would make it scope to the entire action (command execution) + // whereas it should only be scoped to the Execute method. connector.StartUserAction(ConnectorState.Executing, this, CancellationToken.None); Task? sendTask = null; diff --git a/test/Npgsql.Tests/CommandTests.cs b/test/Npgsql.Tests/CommandTests.cs index 0fcf80ae2e..f760b4d437 100644 --- a/test/Npgsql.Tests/CommandTests.cs +++ b/test/Npgsql.Tests/CommandTests.cs @@ -288,6 +288,28 @@ public async Task Cancel() await cancelTask; } + [Test] + public async Task CancelAsyncImmediately() + { + if (IsMultiplexing) + return; // Multiplexing, cancellation + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await using var conn = await OpenConnectionAsync(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + + var t = cmd.ExecuteScalarAsync(cts.Token); + Assert.That(t.IsCompleted, Is.True); // checks, if a query has completed synchronously + Assert.That(t.Status, Is.EqualTo(TaskStatus.Canceled)); + Assert.ThrowsAsync(async () => await t); + + Assert.That(conn.FullState, Is.EqualTo(ConnectionState.Open)); + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + [Test, Description("Cancels an async query with the cancellation token, with successful PG cancellation")] public async Task CancelAsyncSoft() { @@ -296,7 +318,7 @@ public async Task CancelAsyncSoft() await using var conn = await OpenConnectionAsync(); using var cmd = CreateSleepCommand(conn); - var cancellationSource = new CancellationTokenSource(); + using var cancellationSource = new CancellationTokenSource(); var t = cmd.ExecuteNonQueryAsync(cancellationSource.Token); cancellationSource.Cancel(); @@ -322,7 +344,7 @@ public async Task CancelAsyncHard() var processId = conn.ProcessID; - var cancellationSource = new CancellationTokenSource(); + using var cancellationSource = new CancellationTokenSource(); using var cmd = new NpgsqlCommand("SELECT 1", conn); var t = cmd.ExecuteScalarAsync(cancellationSource.Token); cancellationSource.Cancel();