Search code examples
c#postgresqltransactionsado.netiasyncenumerable

How can I correctly use NpgsqlTransaction inside a IAsyncEnumerable emitter function?


I don't need to catch the exception, but I do need to Rollback if there is an exception:

public async IAsyncEnumerable<Item> Select()
{
    var t = await con.BeginTransactionAsync(token);
    try {
        var batchOfItems = new List<Item>(); //Buffer, so the one connection can be used while enumerating items
        using (var reader = await com.ExecuteReaderAsync(SQL, token)) 
        {
            while (await reader.ReadAsync(token))
            {
                var M = await Materializer(reader, token);
                batchOfItems.Add(M);
            }
        }

        foreach (var item in batchOfItems)
        {
            yield return item;
        }

        await t.CommitAsync();
    }
    catch
    {
        await t.RollbackAsync();
    }
    finally
    {
        await t.DisposeAsync();
    }
}

(This code is a simplified version of what I am doing, for illustration purposes)

This fails with the message:

cannot yield a value in the body of a try block with a catch clause


This is similar to Yield return from a try/catch block, but this has novel context:

  • "IAsyncEnumerable" which is relatively new.
  • Postgresql (for which the answer uses an internal property)
  • This question has a better title, explicitly referring to "Transaction" context. Other contexts with the same error message won't have the same answer.

This is not the same as Why can't yield return appear inside a try block with a catch?. In my case, the context is more specific: I need the catch block to Rollback, not to do anything else. Also, as you can see, I already know the answer and created this as a Q&A combo. As you can see from the answer, that answer isn't relevant to Why can't yield return appear inside a try block with a catch?


Solution

  • You can move the Rollback to the finally block if you can check whether or not the transaction was committed, which you can do using IsCompleted

    public async IAsyncEnumerable<Item> Select()
    {
        var t = await con.BeginTransactionAsync(token);
        try {
            var batchOfItems = new List<Item>(); //Buffer, so the one connection can be used while enumerating items
            async using (var reader = await com.ExecuteReaderAsync(SQL, token)) 
            {
                while (await reader.ReadAsync(token))
                {
                    var M = await Materializer(reader, token);
                    batchOfItems.Add(M);
                }
            }
    
            foreach (var item in batchOfItems)
            {
                yield return item;
            }
    
            await t.CommitAsync();
        }
        finally
        {
            if (t.IsCompleted == false) //Implemented on NpgsqlTransaction, but not DbTransaction
                await t.RollbackAsync();
            await t.DisposeAsync();
        }
    }
    

    Note: The catch block has been removed, and the finally block has two lines added to the start.

    This same approach can also work on other DbTransaction implementations that don't have IsCompleted

    see https://stackoverflow.com/a/7245193/887092