Get the most out of Async/Await in C#

Posted by: Yacoub Massad , on 6/3/2022, in Category C#
Views: 21319
Abstract: In this article, I try to show how async/await can be used to solve problems in situations where developers may not think about async/await as an immediate solution.

The async and await keywords were added to the C# language in version 5, about nine years ago. They enable developers to write asynchronous methods.

Note: This article is not an introduction to async/await. I assume the reader already knows what these keywords mean and how to use them.

Editorial Note: For a quick introduction to Async Await in C#, take a look at Async & Await in C# 5.0 and Asynchronous Programming in C# using Async Await – Best Practices

It seems to me that many developers do understand async/await and know how to use them, when they must. For example, if they are assigned to work on an existing piece of code that uses async/await, they know how to work with this existing code.

At times, they could also be forced by the libraries they are using to use async/await. For example, the HttpClient class (before .NET 5) did not have synchronous methods; only asynchronous ones. Even today, it has only a synchronous Send method. It does not have synchronous versions of the more convenient GetAsync, PostAsync, PutAsync, or DeleteAsync methods. This means that when you use the HttpClient class, you are kind of forced to use async/await. You can use .Result or .Wait() on the returned tasks from the async methods of HttpClient to mimic a synchronous experience, but that is not recommended.

However, I have also seen developers miss chances to use this feature when they did not expect async/await to solve the problem. This resulted in complex code that could have been simplified with async/await.

This article is not about how to use async/await when you are supposed to.

Instead, in this article I try to show how you can use async/await to solve problems when you might not have expected it to provide a solution.

A simple Async/Await example: A timer

I will start with a simple example: a timer.

private void StartButton_OnClick(object sender, RoutedEventArgs e)
{
    async Task Start()
    {
        int count = 1;
        
        while (true)
        {            
            this.Title = count + " iterations";

            count++;

     await Task.Delay(1000);
        }
    }

    Start();
} 

This StartButton_OnClick method is an event handler in a WPF application. It runs when a “Start” button is clicked.

This method defines a local function called Start, which it calls in a fire and forget manner. This local function is asynchronous. When this function is run, it basically starts counting from 1 to infinity. Each second, it increments the counter by one and displays the result on the title of the window.

Almost every second, the title of the window will change.

Using Task.Delay, I was able to basically create a timer that runs every second.

If on each iteration we made some calculation that takes half a second, then the counter will be updated every 1.5 seconds.

private void StartButton_OnClick(object sender, RoutedEventArgs e)
{
    async Task Start()
    {
        int count = 1;

        while (true)
        {
            var calculationResult = RunCalculationThatTakesHalfASecond();

            this.Title = count + " iterations and result is :" + calculationResult;

            count++;

            await Task.Delay(1000);
        }
    }

    Start();
}

We can do the following to try to increment the counter every second:

private void StartButton_OnClick(object sender, RoutedEventArgs e)
{
    async Task Start()
    {
        int count = 1;

        while (true)
        {
            var sw = Stopwatch.StartNew();
                    
            var calculationResult = RunCalculationThatTakesHalfASecond();

            this.Title = count + " iterations and result is :" + calculationResult;

            count++;

            var timeToWait = TimeSpan.FromSeconds(1) - sw.Elapsed;

            if (timeToWait > TimeSpan.Zero)
            {
                await Task.Delay(timeToWait);
            }
        }
    }

    Start();
}

In the code above, I measure the time it takes to process each iteration using the Stopwatch class. After processing, I do not delay the next iteration for a full second. Instead, I delay the next iteration one second minus the time spent on processing the current iteration.

Instead of async/await, I can use a timer class like this:

private void StartButton_OnClick(object sender, RoutedEventArgs e)
{
    int count = 1;

    var timer = new System.Windows.Threading.DispatcherTimer();

    timer.Tick += (_, __) =>
    {
        var sw = Stopwatch.StartNew();

        var calculationResult = RunCalculationThatTakesHalfASecond();

        this.Title = count + " iterations and result is :" + calculationResult;

        count++;

        var timeToWait = TimeSpan.FromSeconds(1) - sw.Elapsed;

        timer.Interval = timeToWait > TimeSpan.Zero ? timeToWait : TimeSpan.Zero;
    };
    
    timer.Interval = TimeSpan.FromSeconds(1);
    timer.Start();
}

The DispatcherTimer class is configured to raise the Tick event every second. But it starts measuring time after the Tick event handler completes. Therefore, like before, I am using a Stopwatch to calculate the time each iteration takes and adjusting the timer wait interval for the next iteration.

The difference between the two examples is that in the async/await case, the code is modeled as a single procedure while in the timer class case, we have basically two procedures: the procedure that starts the timer (StartButton_OnClick), and the event handler lambda that runs every time a new iteration begins.

Modeling logic as a single procedure gives us more control and is cleaner.

For example, in the async/await version, I am using a while loop to model the iterations. The logic in the async/await version is clearly modeled using procedural code.

For example, compare how we specify the waiting time for each iteration in the two examples. In the async/await version, we “wait” for the remaining time and continue the loop while in the timer class version, we set the Interval property.

In this later version, it’s like we are communicating state between two different things. The code in the lambda is telling the timer object how much to wait before executing the next iteration. In the async/await version, there is no such communication. There is a single procedure.

I call the approach that uses the timer class an event-based approach because we write code to react to events.

In the async/await version, I can exit the loop using a simple break statement. In the event-based version, I would need to call timer.Stop() which is a form of communication between two things.

In summary, the async/await version allowed us to model the timer logic, while the event-based version required us to coordinate things.

This difference will become more apparent in the next example.

A bigger Async/Await example: A transaction system

In this section, I will share with you a bigger example. You can find the source code here: https://github.com/ymassad/AsyncAwaitExample/tree/main/AsyncAwaitExample

Event Based Approach

The idea here is basically this: we want to provide an ASP.NET Web API service with three APIs to allow consumers of the service to insert multiple pieces of data into the database within a single database transaction.

It is expected that the whole transaction might span several minutes because a few seconds (or minutes) might pass in between posting one piece of data to the server, and the other. Here are the APIs:

[HttpPost]
[Route("Event/StartTransaction")]
public Guid StartTransaction()

[HttpPost]
[Route("Event/Add")]
public void Add(Guid transactionId, [FromBody] DataPointDTO item)

[HttpPost]
[Route("Event/EndTransaction")]
public void EndTransaction(Guid transactionId)

The consumer of the service would start by calling the StartTransaction API to start a new transaction. A unique transaction id is returned to the consumer. The consumer would then call the Add API multiple times, each time passing the transaction id and some piece of data. Once the consumer wants to commit the transaction, the logic would call EndTransaction passing the transaction id.

The three method signatures above are the signatures of actions (methods) in the EventController class in the TransactionWebService1 project. I named this class EventController because it uses the event-based approach I discussed in the previous section.

Although the three methods are not C# event handlers, they are basically events handlers. They get called when an HTTP request arrives. The logic of the transaction is distributed among the tree methods.

I have included the implementation of these methods here for easier understanding of the article:

[HttpPost]
[Route("Event/StartTransaction")]
public Guid StartTransaction()
{
    var transactionId = Guid.NewGuid();

    var connectionString = configuration.GetConnectionString("ConnectionString");

    var context = new DatabaseContext(connectionString);

    context.Database.EnsureCreated();

    statePerTransaction.InitializeState(transactionId, new StateObject(context));

    return transactionId;
}

The StartTransaction method creates a new random transaction id, creates a new Entity Framework Core database context object, makes sure the database is created, and then stores the database context object in an in-memory state object that maps the state to a specific transaction id. This allows us later to retrieve the database context object in the other methods.

Note: In many cases, it is not a good idea to store state in memory in web services. One reason is that web services might be restarted and therefore in-memory state would be lost. Another reason is that you might want to have multiple servers respond to requests, and requests belonging to the same transaction might be processed differently by different servers that don’t share memory. This article is not on web service design. The examples I provide may be valid for some scenarios and not others.

[HttpPost]
[Route("Event/Add")]
public void Add(Guid transactionId, [FromBody] DataPointDTO item)
{
    var stateObject = statePerTransaction.GetStateObjectOrThrow(transactionId);

    try
    {
        var context = stateObject.DatabaseContext;

        var dataPointEntity = new DataPoint()
        {
            Value = item.Value
        };

        context.DataPoints.Add(dataPointEntity);
    }
    catch
    {
        statePerTransaction.RemoveStateObject(transactionId);
        throw;
    }
}

The Add method starts by retrieving the state object based on the transaction id. This state object will contain the database context object. A new DataPoint entity is created based on the data provided to the Add method. Such an entity is then attached to the database context. If there is an error processing the data, the state object will be removed from memory.

[HttpPost]
[Route("Event/EndTransaction")]
public void EndTransaction(Guid transactionId)
{
    var stateObject = statePerTransaction.GetStateObjectOrThrow(transactionId);

    try
    {
        var context = stateObject.DatabaseContext;

        context.SaveChanges();
    }
    finally
    {
        statePerTransaction.RemoveStateObject(transactionId);
    }
}

The EndTransaction method retrieves the state object, invokes SaveChanges on the database context (thus committing the transaction), and finally removes the state object from memory.

Note: The TransactionWebService1 project (and the TransactionWebService2 project) is a console application. You can run it to test running a complete transaction. If you do run it, the ASP.NET controller (AsyncAwaitController or EventController) will be hosted and ready to accept requests, and an HttpClient based consumer will call the APIs to run a complete transaction that includes five data points. See the code in Program.cs to see how to select which controller (AsyncAwaitController or EventController) to use and how the FakeClient class is used to run a complete transaction.

The implementation of the EventController controller seems reasonably clean.

Async/Await Alternative

Now, let’s look at the async/await alternative of the same controller. The code is inside the AsyncAwaitController class.

[HttpPost]
[Route("AsyncAwait/StartTransaction")]
public Guid StartTransaction()
{
    var transactionId = Guid.NewGuid();
    
    HandleTransaction(transactionId);

    return transactionId;
}

[HttpPost]
[Route("AsyncAwait/Add")]
public void Add(Guid transactionId, [FromBody] DataPointDTO item)
{ ... }

[HttpPost]
[Route("AsyncAwait/EndTransaction")]
public void EndTransaction(Guid transactionId)
{ ... }

These three methods have the exact same signature as the ones in the EventController class. The implementation is different though.

The StartTransaction method generates a new random transaction id, and then calls a method named HandleTransaction. The HandleTransaction method is an asynchronous method. The StartTransaction method does not wait for the HandleTransaction method to complete (does not await the returned Task or call the Wait function on the returned task).

Let’s take a look at this HandleTransaction method before we discuss the other two APIs.

private async Task HandleTransaction(Guid transactionId)
{
    statePerTransaction.InitializeState(transactionId, new StateObject(new AsyncCollection<DataPointDTO>()));

    try
    {
        var connectionString = configuration.GetConnectionString("ConnectionString");

        await using var context = new DatabaseContext(connectionString);

        await context.Database.EnsureCreatedAsync();

        while (true)
        {
            var item = await WaitForNextItem(transactionId).ConfigureAwait(false);

            if (item.ended)
            {
                //Transaction complete
                await context.SaveChangesAsync();

                return;
            }
            else
            {
                var dto = item.obj ?? throw new Exception("Unexpected: obj is null");

                var dataPointEntity = new DataPoint()
                {
                    Value = dto.Value
                };
            
                context.DataPoints.Add(dataPointEntity);
            }
        }
    }
    finally
    {
        statePerTransaction.RemoveStateObject(transactionId);
    }
}

The HandleTransaction method is an asynchronous method that models a single transaction from the beginning to the end. It contains all the logic that we saw in the three API methods in the EventController class. It first initializes a state object and maps it to the transaction id. This state object includes an AsyncCollection<DataPointDTO> object. More on this soon.

The HandleTransaction method then creates a new database context object. Notice how we are using the using declaration feature of C# 8 to make sure that the context object is disposed of before the HandleTransaction method exits.

In the EventController class, creating the context object was in the StartTransaction method, and disposing of it was in the EndTransaction method. Just now (as I am writing this), I wanted to check if I am also disposing of in the EventController.Add method in case there is an exception, but I am not.

I forgot.

In the AsyncAwaitController class, since all of the logic is in a single method (HandleTransaction), it’s harder to make this mistake.

Also, the call to RemoveStateObject in HandleTransaction is done once at the end in a finally block. In the EventController class, we had to do that twice: once in the Add method in case there was an exception, and once in the EndTransaction method.

After making sure the database is created, I am doing a loop. In the loop, I am waiting for the next data item to arrive. I am basically waiting for the Add or EndTransaction methods to be called. Let’s go back to the implementations of these methods to see how this works.

[HttpPost]
[Route("AsyncAwait/Add")]
public void Add(Guid transactionId, [FromBody] DataPointDTO item)
{
    statePerTransaction
        .GetStateObjectOrThrow(transactionId)
        .Collection
        .Add(item);
}

[HttpPost]
[Route("AsyncAwait/EndTransaction")]
public void EndTransaction(Guid transactionId)
{
    statePerTransaction
        .GetStateObjectOrThrow(transactionId)
        .Collection
        .CompleteAdding();
}

The Add method simply adds the posted data object (of type DataPointDTO) to the AsyncCollection object associated with the transaction id. The EndTransaction method simply marks the AsyncCollection object as completed (that no more items can be added to it).

The AsyncCollection class is part of a NuGet package called AsyncEx. I don’t want to talk about this class in detail. But you can think of it as a producer consumer collection. Someone can add items to it, and another one can take items from it. The nice thing about this collection is that we can asynchronously wait for items to be available in this collection. The following WaitForNextItem method does exactly that. It is called from the HandleTransaction method.

private async Task<(bool ended, DataPointDTO? obj)> WaitForNextItem(Guid transactionId)
{
    var collection = statePerTransaction.GetStateObjectOrThrow(transactionId).Collection;

    if (!await collection.OutputAvailableAsync())
    {
        return (true, null);
    }

    return (false, collection.Take());
}

The AsyncCollection.OutputAvailableAsync method is an asynchronous method. It returns a Task<bool> that will complete when the collection becomes non-empty or when the collection is marked as completed and there are no more items to take. This method asynchronously returns true if there are available items, and false, if there are no more items and the collection is marked as complete.

In the WaitForNextItem method, we asynchronously return (true, null) if the collection is empty and marked as complete (EndTransaction was called), or (false, item) when there is an available item (Add was called).

Now back to HandleTransaction – in the loop, we handle two cases:

1. There is an item: we add it to the database context object

2. There will be no more items because EndTransaction was called: we call SaveChangesAsync and exit the loop.

I hope you see the value in using async/await in modeling such a relatively long-running operation.

Note: At the end of the Main method in Program.cs, I wait two seconds before exiting the application. This is required because when ASP.NET exits, it doesn’t know about active invocations of HandleTransaction because such invocations are not necessary part of any active requests, and I want to make sure such invocations are complete before I exit the application. There are better ways of handling this. One way is to use an ASP.NET hosted service, but this is out of scope of this article.

Now, let’s say that we want to handle the case where a transaction is abandoned. That is, what if a consumer calls StartTransaction, calls Add a couple of times, and then some time elapses without any more calls to Add or EndTransaction? We don’t want to keep the state in memory, so we want to implement some timeout functionality. That is, if we don’t receive a related request within X seconds, we want to remove the transaction from memory.

With the async/await based implementation, the fix is really easy.

The only line that changes in the HandleTransaction method (see the updated AsyncAwaitController class in the TransactionWebService2 project) is this:

var item = await WaitForNextItem(transactionId)
    .TimeoutAfter(Program.TimeoutSpan)
    .ConfigureAwait(false);

I simply called an extension method named TimeoutAfter on the task returned from WaitForNextItem.

This method returns a new task object that is guaranteed to complete within the specified timespan; either successfully if the original task completed, or unsuccessfully, if the specified timespan elapsed before the original task completes.

Here is the code of this method for your reference:

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    using var timeoutCancellationTokenSource = new CancellationTokenSource();

    var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
    if (completedTask != task)
        throw new TimeoutException();

    timeoutCancellationTokenSource.Cancel();
    return await task;
}

I based this method implementation on the following Stack Overflow answer: https://stackoverflow.com/a/22078975/2290059

Now, if we don’t receive a call to the Add or EndTransaction methods within some period of time (currently 5 seconds as specified by the Program.TimeoutSpan field), an exception will be thrown in the line that calls TimeoutAfter. As per the rules of the C# language, the database context will be disposed of properly and the state will be removed since a call to do so is in the finally block.

Using Event Controller

Now, when I tried to implement this new feature in the event-based controller, it was really hard. You can find the code in the EventController class in the TransactionWebService2 project.

I spent a good amount of time trying to implement the new code in EventController. And I am not really sure if the code I have, covers all edge cases. Here is the code for your reference:

[HttpPost]
[Route("Event/StartTransaction")]
public Guid StartTransaction()
{
    var transactionId = Guid.NewGuid();

    var connectionString = configuration.GetConnectionString("ConnectionString");

    var context = new DatabaseContext(connectionString);

    context.Database.EnsureCreated();

    var reset = TimeoutManager.RunActionAfter(Program.TimeoutSpan, () =>
    {
        try
        {
            context.Dispose();
        }
        finally
        {
            statePerTransaction.RemoveStateObject(transactionId);
        }
    });

    statePerTransaction.InitializeState(transactionId, new StateObject(context, reset));

    return transactionId;
}

[HttpPost]
[Route("Event/Add")]
public void Add(Guid transactionId, [FromBody] DataPointDTO item)
{
    var stateObject = statePerTransaction.GetStateObjectOrThrow(transactionId);
    var context = stateObject.DatabaseContext;

    try
    {
        var dataPointEntity = new DataPoint()
        {
            Value = item.Value
        };

        context.DataPoints.Add(dataPointEntity);
    }
    catch
    {
        try
        {
            context.Dispose();
        }
        finally
        {
            statePerTransaction.RemoveStateObject(transactionId);
        }

        throw;
    }

    stateObject.ResetTimeout(cancel: false);
}

[HttpPost]
[Route("Event/EndTransaction")]
public void EndTransaction(Guid transactionId)
{
    var stateObject = statePerTransaction.GetStateObjectOrThrow(transactionId);

    try
    {
        var context = stateObject.DatabaseContext;

        try
        {
            context.SaveChanges();
        }
        finally
        {
            context.Dispose();
        }
    }
    finally
    {
        statePerTransaction.RemoveStateObject(transactionId);
    }

    stateObject.ResetTimeout(cancel: true);
}

I implemented a new class called TimeoutManager to help me run an action after some time in a resettable way. That is, I want to register an action to run after say 5 seconds, but then I want to be able to reset the timer if I receive an Add request before the time out elapses.

For example, say I receive an Add request, so I start counting from 1. After 4 seconds, I receive a new Add request, which means that I need to reset the timer. I should start counting from 1 again, not continue the counting from 4. See the TimeoutManager class for more details.

With the event controller approach, I am not 100% sure about the correctness of this class.

Observe how I am setting up the cleaning code to run after the timeout period in the StartTransaction method. Also observe how I am resetting the timeout in the Add method. In the EndTransaction method I am cancelling the timeout.

I do not like the code in the updated EventController class. It is not clean, and I am not sure about its correctness. Adding the timeout feature in the AsyncAwaitController was much cleaner and verifying correctness is much easier there.

I hope I was able to show you the value of modeling an asynchronous operation using async/await.

Conclusion:

In this article, I have shown examples of how async/await can be used to model asynchronous operations in a clean way.

There may be asynchronous operations in your code that you are not modeling explicitly as asynchronous. You might be responding to multiple events and coordinating between multiple event handlers using state.

 

If you can detect such implicit asynchronous operations and model them using async/await, you are likely to end up with cleaner code.

Download the entire source code of this article (Github)

This article was technically reviewed by Damir Arh.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Yacoub Massad is a software developer and works mainly on Microsoft technologies. Currently, he works at Zeva International where he uses C#, .NET, and other technologies to create eDiscovery solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at criticalsoftwareblog.com. He is also the creator of DIVEX(https://divex.dev), a dependency injection tool that allows you to compose objects and functions in C# in a way that makes your code more maintainable. Recently he started a YouTube channel about Roslyn, the .NET compiler.



Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!