67

As many knows, TransactionScope were forgotten when the async await pattern was introduced in .Net. They were broken if we were trying to use some await call inside a transaction scope.

Now this is fixed thanks to a scope constructor option.

But it looks to me there is still a missing piece, at least I am unable to find how to do that in a simple "transaction scope like" way: how to await the commit or rollback of a scope?

Commit and rollback are IO operations too, they should be awaitable. But since they happen on scope disposal, we have to await the dispose. That is not doable without having IAsyncDisposable implemented by transaction scopes, which is not currently the case.

I had a look at the System.Transactions.Transaction interface too: no awaitable methods there either.

I understand that committing and rollbacking is almost just sending a flag to the database, so it should be fast. But with distributed transactions, that could be less fast. And anyway, that is still some blocking IO.

About distributed cases, remember this may trigger a two phases commit. In some cases additional durable resources are enlisted during the first phase (prepare). It then usually means some additional queries are issued against those lately enlisted resources. All that happening during the commit.

So is there any way to await a transaction scope? Or a System.Transactions.Transaction instead?

Note: I do not consider this to be a duplicate of "Is it possible to commit/rollback SqlTransaction in asynchronous?". SqlTransaction are more limited than system transactions. They can address only SQL-Server and are never distributed. Some other transactions do have async methods, such as Npgsql. Now for having async methods on transaction scopes/system transaction, DbTransaction may be required to have async methods. (I do not know the internals of system transaction, but it is maybe using this ADO.NET contract. The way we enlist connection into system transaction does let me think it does not use it though.)
Update: DbTransaction does have them in .Net Core 3.0, see #35012 (notably thanks to Roji).

Frédéric
  • 9,364
  • 3
  • 62
  • 112
  • 4
    `using` and `async/await` are, today, competing code generators (https://github.com/dotnet/roslyn/issues/114). Are you talking about an imaginary syntactic sugar? How would you see it? As you say, all happens in the `Dispose()` implementation, and only Microsoft can change that. You can always fire & forget the last `Dispose()` call manually w/o `using` if that's what you're after. – Simon Mourier Jun 01 '17 at 06:53
  • Thanks for the Roslyn thread. It would be a solution, but looks currently stalled. Maybe the `async using` will never be live. But then ressources having IO bound cleanup to do should have some `CloseAsync` method allowing to perform them, and then causing the dispose to have no more IO to perform. Unfortunately scope does not provides this. I am not after a fire&forget, especially about transaction scope disposal since it may fail by design, meaning data was not committed. (This happens in distributed scenario, when code ask for commit but a ressource has vetoed.) A fire&forget would miss it. – Frédéric Jun 01 '17 at 14:47
  • 1
    Async methods weren't forgotten. There never were any non-blocking commit/rollback functions, eg `BeginCommit/EndCommit` for TransactionScope or SqlTransaction. Most likely because a transaction is a hard computation boundary, not just a remote call. – Panagiotis Kanavos Jun 07 '17 at 12:59
  • 2
    @PanagiotisKanavos True, but aren't those non blocking functions missing too? I do not really get why the semantic (hard boundary) should deter from allowing an async call. When async programming was not very practical in .Net, that was maybe not much a concern, but now that it gets more mainstream, maybe is it more debatable to lack them. By the way some other data providers do offer async methods for transaction, see [Npgsql .Net provider](https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/NpgsqlTransaction.cs#L158) by example. – Frédéric Jun 07 '17 at 13:10
  • That's the exception and not part of the ADO.NET API. Oracle and DB2 do *not* have such methods. There is [a duplicate question](https://stackoverflow.com/questions/31152954/is-it-possible-to-commit-rollback-sqltransaction-in-asynchronous) in fact, which shows that the transaction mechanism is explicitly sychronous. The cost is minimal because a *commit* simply commits existing changes. It's the *rollback* that's expensive – Panagiotis Kanavos Jun 07 '17 at 13:21
  • Why are you looking for such methods? A commit shouldn't cost anything. Transactions should be short so even a rollback shouldn't have too much work to do. A rollback would cause delays only if the transaction performed some heavy batch modifications, in which case you don't really care about blocking - you won't/shouldn't be running a lot of heavy data manipulation jobs concurrently – Panagiotis Kanavos Jun 07 '17 at 13:23
  • 5
    Well, going that route, why having bothered having async methods on sql objects at all? An IO never cost nothing, especially when network comes into play, even if the payload is tiny and the process to be carried on distant server too. There are even async methods on datareader for reading a single column value or just [testing whether a column is `null`](https://msdn.microsoft.com/en-us/library/hh462664.aspx). Granted, their are truly async only with a dedicated mode, useful mostly when the row contains blobs. But as tiny as an `IsNull` test should be, it has still an async version available. – Frédéric Jun 07 '17 at 13:41
  • 4
    And as for the why, it is just for curiosity. Is it that bad to seek underlying reasons to a design? The near dup you point to has an answer having seemingly thoroughly analyzed the `SqlTransaction` code, but that does not really give the rationals behind that, excepted considering it should not be a very long synchronous block. That reason does not seem enough alone, since there is an async method just for testing null on a single column of a result set. – Frédéric Jun 07 '17 at 13:47
  • @PanagiotisKanavos `Commit` itself may cost nothing, but there's an event `TransactionCompleted` which is raised synchronously with `Commit`. So handler is a kind of implicit continuation. For example I may need to send Domain Events not before, but after successful commit. This operation may last long and ideally should be asynchronous. However, using `async void` in a handler is not an option due to introduction of concurrency with the code that runs immediately after call to `Commit`. – Pavel Voronin Nov 10 '18 at 19:50
  • @PavelVoronin it's not an implicit continuation. If you have concurrency problems it's a problem with the implementation. Domain/Busines events are meant to be asynchronous in the domain/business sense, able to be replayed. Sounds like the implementation uses *implementation* concepts like transactions and event handlers in place of actual domain concepts like UoW and domain events. A domain event is *far* more likely to be implemented as a DTO and handled through queues – Panagiotis Kanavos Nov 12 '18 at 07:29
  • @PanagiotisKanavos. Exactly, I want to publish domain events after UoW commits. But ambient transaction allows to run multiple scenarios where UoW’s Commit will be called, but in reality actual commit will happen on `Complete()`. That would be a time to publish events. – Pavel Voronin Nov 12 '18 at 07:51
  • @PavelVoronin that's the problem. UoW is *not* a transaction. Starting a transaction when a UoW starts is in fact one of the worst perf killers. In DDD *storage* is an implementation detail and *database transactions* are a feature of that detail. What if a *NoSQL* database was used? Or a file? A queue? – Panagiotis Kanavos Nov 12 '18 at 07:55
  • @PanagiotisKanavos Sure, it is. But implementation may be either good or bad nevertheless. Absence of async commit and callbacks somewhat prevents good implementation. However, I agree, that the need for 'large' transactions is a design smell. – Pavel Voronin Nov 12 '18 at 09:10
  • 1
    @frédéric I don't see any reasonable way to compensate for lack of these async APIs. If you care about them being added, please create an issue at https://github.com/dotnet/corefx/issues. It is not going to happen for .NET Framework but they could be added in .NET Core if there is really enough interest. IAsyncDisposable is planned for C# 8/.NET Core 3.0, so that part could be implemented too. Also related, .NET Core lacks support for distributed transactions (https://github.com/dotnet/corefx/issues/16755). Although we haven't seen much demand, that could change with .NET Core 3.0. – divega Nov 20 '18 at 19:01
  • @divega, thanks for the information. At the time of the question, async support work was ongoing in NHibernate, and I was willing to make sure we were not missing something in the case of transaction scope. Having those async APIs would be a nice to have, but I do not care enough about them for asking for them. Still it seems some other people do care more, since this question has got many up-votes. – Frédéric Nov 20 '18 at 23:49

2 Answers2

10

There's no way to implement it so far. But they work on it

Gleb
  • 1,723
  • 1
  • 11
  • 24
  • I should have opened that one myself. But no, they are not working on it, they just do not reject it yet. `We didn't have plans to do this, but we will consider it for 5.0.0. We need to do some investigation first to see how complicated the work will be.` – Frédéric Dec 12 '19 at 13:12
  • @Frédéric well, as they said - they considering and investigating and it's also a work. And I don't think they'll just throw it away. Let's hope for the best. – Gleb Dec 12 '19 at 13:17
  • He only states they need to investigate. That does not mean anyone is currently investigating. They tend to assign people to issues when they actually start anything on it. Currently it has no one assigned. I am a bit pessimist on this one due to the mess the distributed case [seems to be](https://github.com/nhibernate/nhibernate-core/pull/627#issuecomment-308698162).Thanks anyway for pointing this issue. – Frédéric Dec 12 '19 at 13:28
  • 1
    And now, they have removed it from the 5.0.0 milestone and put it in the backlog (Future milestone). – Frédéric Jul 28 '20 at 21:20
0

Maybe a late answer, but what you want basically boils down to a kind of syntactic sugar that can be easily created on your own.

Generalizing your problem, I implemented an "async using" syntax, which allows both the body and the "dispose" part of the "using" to be awaitable. Here is how it looks:

async Task DoSomething()
{ 
    await UsingAsync.Do(
        // this is the disposable usually passed to using(...)
        new TransactionScope(TransactionScopeAsyncFlowOption.Enabled), 
        // this is the body of the using() {...}
        async transaction => {
            await Task.Delay(100);   // do any async stuff here...
            transaction.Complete();  // mark transaction for Commit
        } // <-- the "dispose" part is also awaitable
    );
}

The implementation is as simple as this:

public static class UsingAsync
{
    public static async Task Do<TDisposable>(
        TDisposable disposable, 
        Func<TDisposable, Task> body)
        where TDisposable : IDisposable
    {
        try
        {
            await body(disposable);
        }
        finally
        {
            if (disposable != null)
            {
                await Task.Run(() => disposable.Dispose());
            }
        }
    }
}

There is a difference in error handling, compared to the regular using clause. When using UsingAsync.Do, any exception thrown by the body or the dispose will be wrapped inside an AggregateException. This is useful when both body and dispose each throw an exception, and both exceptions can be examined in an AggregateException. With the regular using clause, only the exception thrown by dispose will be caught, unless the body is explicitly wrapped in try..catch.

felix-b
  • 8,178
  • 1
  • 26
  • 36
  • 3
    No, I do not want that. I want a way to asynchronously commit or rollback a transaction scope (or a system transaction). You do not answer that point. It seems there are no way to do that. I do not want to just encapsulate it in some awaitable wrapper (`Task.Run` in your code) while the scope is still synchronously committed indeed. That would be just overhead without actually releasing the thread used for commit during the call to server. – Frédéric Oct 29 '17 at 17:39
  • Yep. What I did releases your thread and uses another one to perform commit/rollback. What you want requires TransactionScope to truly support async, which is not the case. Probably the only option is implementing own version of TransactionScope that truly supports async -- after all, it is just a wrapper around MSDTC API. – felix-b Oct 29 '17 at 18:07
  • 3
    "await Task.Run(() => disposable.Dispose());" << BAD. The whole entire purpose of using 'await' is to RELEASE YOUR CURRENT THREAD back to the threadpool while waiting on I/O operation to complete so some other task can use the thread. That's why it's a carefully managed pool. All you're doing here with this await Task.Run is freeing your current thread while CONSUMING ANOTHER ONE which you're then blocking with Dispose. You're blocking one thread instead of another, which does not achieve anything. Returning one thread to pool only to grab another and block it makes no sense at all. – Triynko Feb 12 '19 at 18:49
  • @Triynko yep :) that's what I tried to point out in my comment above: OP's intent cannot be solved without inventing a truly async version of TransactionScope. – felix-b Feb 24 '19 at 07:19