DapperLib / DapperLib/Dapper

QueryUnbufferedAsync cancels command unnecessarily for Npgsql, causing performance degradation

Open
#2,188 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
18.4k
Forks
3.7k
Avg merge
5h 8m
Merged PRs (30d)
1

Description

Description

A change introduced in version 2.1.66 to cancel the command in a finally block (https://github.com/DapperLib/Dapper/blob/main/Dapper/SqlMapper.Async.cs#L1336) causes significant performance degradation when used with the Npgsql provider (PostgreSQL).

The issue is that for NpgsqlDataReader, the reader.IsClosed property returns false even after all data has been completely read and the reader is functionally finished. This triggers the explicit cmd.Cancel() in the finally block unnecessarily.

For PostgreSQL, canceling a command is not a lightweight operation. It requires opening a new physical connection to the server to send the cancellation request. This leads to:

  1. Performance overhead on the application side due to the extra connection setup.
  2. Performance overhead on the database server, which has to process the cancellation command and abort the already-finished query.
  3. Connection pool churn, as the cancellation process uses up additional connections temporarily.

This behavior negates the performance benefits of using Unbuffered asynchronous reads.

Proposed Solution

The cancellation logic should be more intelligent. Instead of relying solely on !reader.IsClosed, it should also consider if the reader has already consumed the entire result set naturally.

A better condition might be to check !reader.IsClosed && !reader.NextResult(). Alternatively, a more sophisticated approach could be taken to determine if the reader has been fully enumerated without being explicitly closed.

Steps to Reproduce
  1. Use Dapper 2.1.66+ with Npgsql.
  2. Execute a QueryUnbufferedAsync query.
  3. Observe in SQL logs or with a profiler (like log_statement = 'all' in postgresql.conf) that a CANCEL command is issued for the query even after it has completed successfully.

This behavior is specific to how the Npgsql provider works. Other providers (e.g., SQL Server) may correctly set IsClosed to true after full enumeration, making the Cancel() call a no-op and avoiding this performance issue.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Inspect Dapper/SqlMapper.Async.cs around line 1336 and trace QueryUnbufferedAsync's finally-block cancellation behavior. Reproduce with Npgsql and PostgreSQL logging or a profiler, then verify that fully enumerated results do not trigger an unnecessary cancellation while incomplete reads retain the existing behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, postgresql
Domain
backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.