Volatile IEnlistmentNotification, TransactionScope.AsyncFlowEnabled = true and complex async / await

This is the next question to the next question:

Volatile IEnlistmentNotification and TransactionScope.AsyncFlowEnabled = true

The approach taken in the question above works as long as you don't have to wait for multiple statements. Let me show you an example:

public class SendResourceManager : IEnlistmentNotification
{
    private readonly Action onCommit;

    public SendResourceManager(Action onCommit)
    {
        this.onCommit = onCommit;
    }

    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
        preparingEnlistment.Prepared();
    }

    public void Commit(Enlistment enlistment)
    {
        Debug.WriteLine("Committing");
        this.onCommit();
        Debug.WriteLine("Committed");
        enlistment.Done();
    }

    public void Rollback(Enlistment enlistment)
    {
        enlistment.Done();
    }

    public void InDoubt(Enlistment enlistment)
    {
        enlistment.Done();
    }
}

public class AsyncTransactionalMessageSender : ISendMessagesAsync
{
    private readonly List<Message> sentMessages = new List<Message>();

    public IReadOnlyCollection<Message> SentMessages
    {
        get { return new ReadOnlyCollection<Message>(this.sentMessages); }
    }

    public async Task SendAsync(Message message)
    {
        if (Transaction.Current != null)
        {
            await Transaction.Current.EnlistVolatileAsync(
                new SendResourceManager(async () => await this.SendInternal(message)), 
                EnlistmentOptions.None);
        }
        else
        {
            await this.SendInternal(message);
        }
    }

    private async Task SendInternal(Message message)
    {
        Debug.WriteLine("Sending");
        await Task.Delay(1000);
        this.sentMessages.Add(message);
        Debug.WriteLine("Sent");
    }
}

    [Test]
    public async Task ScopeRollbackAsync_DoesntSend()
    {
        var sender = new AsyncTransactionalMessageSender();
        using (var tx = new System.Transactions.TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
        {
            await sender.SendAsync(new Message("First"));
            await sender.SendAsync(new Message("Second"));
            await sender.SendAsync(new Message("Last"));

            // We do not commit the scope
        }

        sender.SentMessages.Should().BeEmpty();
    }

    [Test]
    public async Task ScopeCompleteAsync_Sends()
    {
        var sender = new AsyncTransactionalMessageSender();
        using (var tx = new System.Transactions.TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
        {
            await sender.SendAsync(new Message("First"));
            await sender.SendAsync(new Message("Second"));
            await sender.SendAsync(new Message("Last"));

            tx.Complete();
        }

        sender.SentMessages.Should().HaveCount(3)
            .And.Contain(m => m.Value == "First")
            .And.Contain(m => m.Value == "Second")
            .And.Contain(m => m.Value == "Last");
    }

      

Once you enter Task.Delay as shown in the example above, the generated asynchronous statemachine will never return and will call this.sentMessages.Add(message)

andDebug.WriteLine("Sent")

The problem is that I can now see a way to correctly borrow async code in the assignment notice. Any ideas how to solve this problem?

0


source to share





All Articles