DapperLib / DapperLib/Dapper

Queryasync method CancellationToken request cancellation, DAPPER will not immediately terminate execution

Open
#1,640 2 comments 3 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

Hello, author: I am a developer from China. My English is not very good, so my question is translated using translation software, so the meaning may not be very accurate.
When I used asynchronous query, I found a small problem. The source code is as follows

private static async Task<IEnumerable<T>> QueryAsync<T>(this IDbConnection cnn, Type effectiveType, CommandDefinition command)
        {
            object param = command.Parameters;
            var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType());
            var info = GetCacheInfo(identity, param, command.AddToCache);
            bool wasClosed = cnn.State == ConnectionState.Closed;
            var cancel = command.CancellationToken;
            using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader);
            if (cancel.CanBeCanceled)
            {
                cancel.Register((obj) => ((DbCommand)obj)?.Cancel(), cmd);
            }
            DbDataReader reader = null;
            try
            {
                if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false);
                reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancel).ConfigureAwait(false);

                var tuple = info.Deserializer;
                int hash = GetColumnHash(reader);
                if (tuple.Func == null || tuple.Hash != hash)
                {
                    if (reader.FieldCount == 0)
                        return Enumerable.Empty<T>();
                    tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false));
                    if (command.AddToCache) SetQueryCache(identity, info);
                }

                var func = tuple.Func;

                if (command.Buffered)
                {
                    var buffer = new List<T>();
                    var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
                    while (await reader.ReadAsync(cancel).ConfigureAwait(false))
                    {
                        object val = func(reader);
                        buffer.Add(GetValue<T>(reader, effectiveType, val));
                    }
                    while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ }
                    command.OnCompleted();
                    return buffer;
                }
                else
                {
                    // can't use ReadAsync / cancellation; but this will have to do
                    wasClosed = false; // don't close if handing back an open reader; rely on the command-behavior
                    var deferred = ExecuteReaderSync<T>(reader, func, command.Parameters);
                    reader = null; // to prevent it being disposed before the caller gets to see it
                    return deferred;
                }
            }
            finally
            {
                using (reader) { /* dispose if non-null */ }
                if (wasClosed)
                    cnn.Close();
            }
        }

When I execute a time-consuming SQL statement, if the request is canceled, that is, the value of CancellationToken.IsCancellationRequested becomes true, then the try will be jumped out and the finally block will be executed. When the reader.dispose under finally is executed, the execution will be blocked until The SQL statement is completely executed before it will continue to execute. What I think is that the request to cancel finally will not block.
Then I checked the source code from the Internet and found a solution:

 private static async Task<IEnumerable<T>> QueryAsync<T>(this IDbConnection cnn, Type effectiveType, CommandDefinition command)
        {
            object param = command.Parameters;
            var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType());
            var info = GetCacheInfo(identity, param, command.AddToCache);
            bool wasClosed = cnn.State == ConnectionState.Closed;
            var cancel = command.CancellationToken;
            using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader);
            if (cancel.CanBeCanceled)
            {
                cancel.Register((obj) => ((DbCommand)obj)?.Cancel(), cmd);
            }
            DbDataReader reader = null;
            try
            {
                if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false);
                reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancel).ConfigureAwait(false);

                var tuple = info.Deserializer;
                int hash = GetColumnHash(reader);
                if (tuple.Func == null || tuple.Hash != hash)
                {
                    if (reader.FieldCount == 0)
                        return Enumerable.Empty<T>();
                    tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false));
                    if (command.AddToCache) SetQueryCache(identity, info);
                }


After adding cancel.Register((obj) => ((DbCommand)obj)?.Cancel(), cmd);, the program will execute smoothly

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

Start at the QueryAsync method shown in the issue and reproduce cancellation with a long-running SQL statement. Trace ExecuteReaderWithFlagsFallbackAsync, ReadAsync, the cancellation registration, and reader disposal; done means the reported cancellation path no longer blocks while normal query cleanup remains intact.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, sql
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.