2

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?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Kind Contributor
  • 17,547
  • 6
  • 53
  • 70
  • This is not the same as https://stackoverflow.com/questions/346365/why-cant-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. – Kind Contributor Sep 19 '20 at 16:17
  • What's the point of using `IAsyncEnumerable` here? As far as I can see, it's only making your code more complex than it should be – Camilo Terevinto Sep 19 '20 at 16:20
  • @CamiloTerevinto "(This code is a simplified version of what I am doing, for illustration purposes)" thanks – Kind Contributor Sep 19 '20 at 16:23
  • The first thing is that you didn't answer my question, and a simplification of code shouldn't change its meaning. The second thing is that just because you want a specific behavior on `catch`, it doesn't at all mean that the duplicate doesn't apply here – Camilo Terevinto Sep 19 '20 at 16:32
  • 1
    My question was just closed as duplicate with no listing of which moderators I need to contact. If you look at my comment I already stated how this is different to https://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch. Despite that, this it was closed without justifying why here in the comments. That's just inconsiderate and unkind. – Kind Contributor Sep 19 '20 at 16:40
  • 1
    @CamiloTerevinto the [duplicate](https://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch) is about *why*. This question is about *how*. It is a relevant but different question. – Theodor Zoulias Sep 19 '20 at 16:46
  • 1
    @CamiloTerevinto I hope I can convince you I'm a genuine hard working person who checks other similar questions thoroughly. It would seem you have jumped the gun in closing my question. I created this as a self-learning Q&A combo - answering my own question for the benefit of others. Mine is quite different, please have a look at my answer; which would not work on that other similar question, where the exception object is to be used for something `Console.WriteLine(e.Message);`. – Kind Contributor Sep 19 '20 at 16:49
  • @TheodorZoulias The answer posted here is based on the answer that Jon Skeet gave on the duplicate. In simple terms: *you can't do this, find an alternative*. In the simplest example (and to demonstrate this Q&A isn't needed): it's enough to have a `bool` variable set to `true` at the end of the `try` block and check its value in the `finally` block – Camilo Terevinto Sep 19 '20 at 16:57
  • 1
    @CamiloTerevinto The use of a boolean latch won't work on the other question. I would have answered there if that worked. Having a boolean latch doesn't capture the exception for error logging (Console.WriteLine). – Kind Contributor Sep 19 '20 at 17:01
  • You're right, it's not *100% identical*. I've edited the duplicate target, took me 2 seconds to find it :) As a note, this is the query I used: `site:stackoverflow.com yield in try catch` – Camilo Terevinto Sep 19 '20 at 17:05
  • 1
    @CamiloTerevinto Good find, the question title you link to is too broad and it's marked as a duplicate. Where it leads is every broader. It's a bit of a mess. This duplicate marking isn't helping. – Kind Contributor Sep 19 '20 at 17:11
  • The duplicate target in the linked question is *terrible* IMO. I could agree to reopen that question. As for the question issues, I'd strongly suggest you to edit it into better shape – Camilo Terevinto Sep 19 '20 at 17:13
  • Another suggestion I would find better, is to post a new Q&A that is more generic and use that as a canonical reference – Camilo Terevinto Sep 19 '20 at 17:15

3 Answers3

2

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

Kind Contributor
  • 17,547
  • 6
  • 53
  • 70
  • Are you sure that the [`SqlTransaction`](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqltransaction) class has an `IsCompleted` property? – Theodor Zoulias Sep 19 '20 at 16:52
  • @TheodorZoulias I'm using postgres NpgsqlTransaction. I'll add that to the question tags. Good pickup - thanks. – Kind Contributor Sep 19 '20 at 16:53
  • As a side note, you could use the [`await using`](https://stackoverflow.com/questions/58791938/c-sharp-8-understanding-await-using-syntax) syntax, so that you don't have to deal with explicitly disposing the `IAsyncDisposable` object. – Theodor Zoulias Sep 19 '20 at 17:11
  • 1
    @TheodorZoulias Thanks, done. I'm not used to IAsyncEnumerable and IAsyncDisposable yet, this is the first time I'm using them. – Kind Contributor Sep 19 '20 at 17:13
  • An interesting excerpt from the [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.data.common.dbtransaction.dispose#remarks): *Dispose should rollback the transaction. However, the behavior of Dispose is provider specific, and should not replace calling Rollback.* – Theodor Zoulias Sep 19 '20 at 17:17
  • 1
    Yeah, I did look up the implementation for Pg, and it did not seem to rollback. Ideally, the .Net team would find a way to catch over a yield. – Kind Contributor Sep 19 '20 at 17:21
1

DbTransaction is considered the best way to manage transactions on SqlConnections, but TransactionScope is also valid, and might help others in related scenarios

public async IAsyncEnumerable<Item> Select()
{
    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        con.EnlistTransaction(Transaction.Current); //it's better to open the connection here, then dispose, but this will work
        com = con.CreateCommand(); //Probably need a new command object so it has the transaction context
        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;
        }

        scope.Complete(); //Asynch option not available
        //No need to have explicit rollback call, instead it's standard for that to happen upon disposal if not completed
    }
}
Kind Contributor
  • 17,547
  • 6
  • 53
  • 70
0

An alternative to creating the IAsyncEnumerable by using a C# iterator could be to use the third-party library AsyncEnumerator (package).

This library was the main resource for creating asynchronous enumerables before the advent of C# 8, and it may still be useful because AFAIK it doesn't suffer by the limitations of the native yield. You are allowed to have try, catch and finally blocks in the body of the lambda passed to the AsyncEnumerable constructor, and invoke the yield.ReturnAsync method from any of these blocks.

Usage example:

using Dasync.Collections;

//...

public IAsyncEnumerable<Item> Select()
{
    return new AsyncEnumerable<Item>(async yield => // This yield is a normal argument
    {
        await using var transaction = await con.BeginTransactionAsync(token);
        try
        {
            var batchOfItems = new List<Item>();
            await 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)
            {
                await yield.ReturnAsync(item); // Instead of yield return item;
            }
            await transaction.CommitAsync();
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync();
        }
    });
}

The yield in the above example is not the C# yield contextual keyword, but just an argument with the same name. You could give it another name if you want.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104