【问题标题】:How can I correctly use NpgsqlTransaction inside a IAsyncEnumerable emitter function?如何在 IAsyncEnumerable 发射器函数中正确使用 NpgsqlTransaction?
【发布时间】:2021-01-06 06:51:41
【问题描述】:

我不需要捕获异常,但是如果有异常我确实需要回滚:

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();
    }
}

(此代码是我正在做的简化版本,用于说明目的)

失败并显示以下消息:

不能在带有 catch 子句的 try 块的主体中​​产生值


这类似于Yield return from a try/catch block,但它具有新颖的上下文:

  • “IAsyncEnumerable”相对较新。
  • Postgresql(答案使用内部属性)
  • 这个问题有一个更好的标题,明确提到“交易”上下文。具有相同错误消息的其他上下文不会有相同的答案。

这与Why can't yield return appear inside a try block with a catch? 不同。就我而言,上下文更具体:我需要 catch 块来回滚,而不是做任何其他事情。此外,如您所见,我已经知道答案并将其创建为问答组合。从答案中可以看出,该答案与Why can't yield return appear inside a try block with a catch?

无关

【问题讨论】:

  • 我的问题刚刚被关闭,因为没有列出我需要联系的版主。如果您查看我的评论,我已经说明了这与stackoverflow.com/questions/346365/… 有何不同。尽管如此,它在没有说明为什么在 cmets 中被关闭的情况下被关闭。这只是不体谅和不友善。
  • @CamiloTerevinto duplicate 是关于为什么。这个问题是关于如何。这是一个相关但不同的问题。
  • @CamiloTerevinto 我希望我能说服你我是一个真正勤奋的人,他会彻底检查其他类似的问题。看来您在结束我的问题时已经过激了。我将其创建为自学问答组合 - 为了他人的利益回答我自己的问题。我的完全不同,请看我的回答;这不适用于其他类似的问题,其中异常对象将用于Console.WriteLine(e.Message);
  • @CamiloTerevinto 使用布尔锁存器不适用于其他问题。如果可行,我会在那里回答。具有布尔锁存器不会捕获错误日志记录 (Console.WriteLine) 的异常。
  • @CamiloTerevinto 很好的发现,您链接到的问题标题太宽泛,被标记为重复。它引领的地方更广泛。这有点乱。这种重复标记没有帮助。

标签: c# postgresql transactions ado.net iasyncenumerable


【解决方案1】:

如果您可以检查事务是否已提交,您可以将回滚移动到 finally 块,您可以使用 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();
    }
}

注意:catch 块已被移除,finally 块在开头添加了两行。

同样的方法也适用于没有 IsCompleted 的其他 DbTransaction 实现

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

【讨论】:

  • 您确定SqlTransaction 类具有IsCompleted 属性吗?
  • @TheodorZoulias 我正在使用 postgres NpgsqlTransaction。我会将其添加到问题标签中。好皮卡 - 谢谢。
  • 附带说明,您可以使用await using 语法,这样您就不必处理显式处理IAsyncDisposable 对象。
  • @TheodorZoulias 谢谢,完成。我还不习惯 IAsyncEnumerable 和 IAsyncDisposable,这是我第一次使用它们。
  • 是的,我确实查找了 Pg 的实现,但它似乎没有回滚。理想情况下,.Net 团队会找到一种方法来获得收益。
【解决方案2】:

使用 C# iterator 创建 IAsyncEnumerable 的替代方法是使用第三方库 AsyncEnumerator (package)。

这个库是在 C# 8 出现之前创建异步枚举的主要资源,它可能仍然有用,因为 AFAIK 它不受本机 yield 的限制。您可以将 lambda 主体中的 trycatchfinally 块传递给 AsyncEnumerable 构造函数,并从这些块中的任何一个调用 yield.ReturnAsync 方法。

使用示例:

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();
        }
    });
}

上面例子中的yield不是C#的yield contextual keyword,只是一个同名的参数。如果你愿意,你可以给它另一个名字。

【讨论】:

  • 这是一个有效的好主意,谢谢!这与我无关,因为我倾向于非常喜欢基类,并避免使用包。
  • @Todd 我也是!但是,如果您想在 Select 方法中捕获异常以便记录它,您会怎么做?
  • 非常正确。这不是这个问题的要求,但它确实提供了更广泛的范围。
【解决方案3】:

DbTransaction 被认为是管理 SqlConnections 上事务的最佳方式,但 TransactionScope 也是有效的,并且可能在相关场景中帮助其他人

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
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-12-16
    • 2014-11-03
    • 1970-01-01
    • 2012-05-01
    • 2017-08-19
    • 2022-01-21
    • 2023-01-16
    • 2015-12-14
    相关资源
    最近更新 更多