Search code examples
c#asynchronoustaskdappercontinuewith

Dapper - Task Cancelled when continuing an asynchronous operation of fetching objects


I'm working with .Net Core 5.0 and Dapper as ORM.

I have the following c# code:

    public Task<IEnumerable<FooViewModel>> FetchAllFoos1(CancellationToken cancel = default)
    {
        string sql = "SELECT * FROM Foos";

        var context = new DbContext();
        var connection = context.GetConnection();
        var cmd = new CommandDefinition(sql, cancellationToken: cancel);

        return connection.QueryAsync<Foo>(cmd)
            .ContinueWith(x => x.Result.Select(y => ToFooViewModel(y)), cancel);
    }

This code is working perfectly.

But this one not and I don't understand why:

    public Task<IEnumerable<FooViewModel>> FetchAllFoos2(CancellationToken cancel = default)
    {
        string sql = "SELECT * FROM Foos";

        using (var context = new DbContext())
        {
            using (var connection = context.GetConnection())
            {
                 var cmd = new CommandDefinition(sql, cancellationToken: cancel);

                 return connection.QueryAsync<Foo>(cmd)
                        .ContinueWith(x => x.Result.Select(y => ToFooViewModel(y)), cancel);
            }
        }
    }

When awaiting the result of FetchAllFoos2: var result = await FetchAllFoos2(), i have a Task Cancelled exception. It happens in the ContinueWith, when it's trying to get x.Result.

I know that the issue come from because i'm using "using" that close the context/connection, but i don't undestand the inner reasons of the exception. I like to use "using" to make sure that any disposable object is cleaned when i'm over the control of the using, but it seems that i cannot use it here..

Can you help me to understand ?

Thank you.


Solution

  • As TheGeneral pointed out, the core problem is that you're using the dangerous, low-level ContinueWith method. As a general rule, use await instead of ContinueWith.

    public async Task<IEnumerable<FooViewModel>> FetchAllFoos2(CancellationToken cancel = default)
    {
        string sql = "SELECT * FROM Foos";
    
        using (var context = new DbContext())
        {
            using (var connection = context.GetConnection())
            {
                 var cmd = new CommandDefinition(sql, cancellationToken: cancel);
    
                 var result = await connection.QueryAsync<Foo>(cmd);
                 return result.Select(y => ToFooViewModel(y));
            }
        }
    }
    

    One of the problems of skipping async and await is that things like disposal happen at an incorrect time. For the async method, the disposals happen after the data is retrieved (after the await). For the non-async method, the disposals happen after the query is started but (possibly) before it returns its data.