In this article, we are going to learn about the various patterns to implement asynchronous programming in .NET.

.NET has had in-built support for asynchronous programming since the release of .NET Framework 1.1. It has been through various improvements over the years, and today it’s a powerful mainstream programming paradigm.

To download the source code for this article, you can visit our GitHub repository.

Let’s start by understanding what asynchronous programming is.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Asynchronous Programming

Asynchronous programming enables multiple operations to run concurrently without waiting for any other operations to complete. Besides, it has a non-blocking nature where the program continues to execute other tasks while executing long-running operations.

Check out our article on Asynchronous Programming Using TAP to know more.

Currently, TAP is the recommended pattern for implementing asynchronous programming in .NET. Although APM and EAP are now legacy models, it’s important to understand them when dealing with legacy projects or libraries.

There are 3 patterns for implementing asynchronous programming in .NET:

  • Asynchronous Programming Model (APM)
  • Event-Based Asynchronous Pattern (EAP)
  • Task-Based Asynchronous Pattern (TAP)

So, let’s now dive into each of these.

Asynchronous Programming Model (APM)

.NET Framework 1.1 introduced asynchronous programming with APM. This model uses the IAsyncResult design pattern to perform asynchronous programming.

IAsyncResult interface is the return type of a method that initiates an asynchronous operation while a method that concludes an asynchronous operation receives this as a parameter.

For example, to implement an asynchronous operation named OperationName using IAsyncResult pattern, we use two methods named BeginOperationName() and EndOperationName(). These methods denote the beginning and end of the asynchronous operation.

The BeginOperationName() method starts the asynchronous operation and returns an output type that implements the IAsyncResult interface. In addition to operation parameters, it can accept a callback method that fires on operation completion. We can also pass an operation context state where we normally pass any dependencies.

Asynchronous Programming Implementation

The main building blocks for creating an APM implementation are:

  • Two methods to start and end asynchronous operations
  • A delegate of AsyncCallback type that fires on operation completion
  • Optional state property that holds dependencies

A typical example of the APM model is the FileStream class in System.IO namespace. It defines BeginRead() and EndRead() methods to asynchronously read bytes from a file. Let’s see how to use them:

public class ApmFileReader
{
    private byte[]? _buffer;
    private const int InputReportLength = 1024;
    private FileStream? _fileStream;

    public void BeginReadAsync()
    {
        _buffer = new byte[InputReportLength];
        _fileStream = File.OpenRead("User.txt");
        _fileStream.BeginRead(_buffer, 0, InputReportLength, ReadCallbackAsync, _buffer);
    }

    public void ReadCallbackAsync(IAsyncResult iResult)
    {
        _fileStream?.EndRead(iResult);
        var buffer = iResult.AsyncState as byte[];
    }
}

The BeginRead() method of the FileStream class accepts an optional state parameter and a callback of AsyncCallback delegate type:

delegate void AsyncCallback(IAsyncResult ar);

For this purpose, we define the ReadCallbackAsync() method. Within this method, we call the EndRead() method of FileStream class to wait for the pending asynchronous read operation to complete. 

The callback method also receives a reference to an IAsyncResult object. Hence, it can query the AsyncState property of the IAsyncResult interface to obtain the reference of the state object. 

Now, we define the BeginReadAsync() method to read the file asynchronously. We start the asynchronous operation by invoking the BeginRead() method of the FileStream class. 

Event-Based Asynchronous Pattern (EAP)

As the name implies, with EAP, we use events to implement asynchronous operations. The operations execute in separate threads, and we use events to notify the completion status.

Check out our article on Events in C# if you are new to this topic.

Applications with UI components mainly use EAP for implementations. This model keeps the UI components responsive while running time-consuming operations as the components don’t need to wait for the operations to complete.

A long-running operation executes in a separate thread making the UI thread free. The UI thread subscribes to the event that notifies the completion of the long-running operation by registering a callback method. Thus, the UI doesn’t need to wait till the operation is complete.

EAP is not just limited to UI components, we can use it to implement various operations asynchronously. For instance, downloading a file, web service calls, etc.

.NET Framework 2.0 introduced EAP. Similar to APM, even EAP is a legacy model as Microsoft recommends using TAP.

Asynchronous Programming Implementation

To implement an asynchronous operation with EAP, we first need an event, OperationNameCompleted that fires on operation completion. 

Next, we need a custom event argument OperationNameEventArgs where we can define custom properties to store any operations results and event statuses.

Finally, we create the asynchronous operation using OperationNameAsync() method and pass any required parameters to run the asynchronous operation.

Let’s see this in practice by implementing an asynchronous operation that fetches user information.

EAP Code Example

First, let’s create a simple User class that holds user attributes:

public class User
{
    public int Id { get; set; }

    public string? Name { get; set; }
}

Secondly, we create a UserService that emulates a long-running operation:

public class UserService
{
    private readonly List<User> _users = new List<User>
    {
        new User { Id = 100, Name = "Adam"},
        new User { Id = 101, Name = "Eve"}
    };

    public User GetUser(int userId)
    {
        // Long-running operation
        return _users.FirstOrDefault(x => x.Id == userId);
    }
}

Now, we define the event argument:

public class GetUserCompletedEventArgs : AsyncCompletedEventArgs
{
    private User _result;

    public User Result
    {
        get
        {
            RaiseExceptionIfNecessary();
            return _result;
        }
    }

    public GetUserCompletedEventArgs(Exception error, bool cancelled, User user)
        : base(error, cancelled, user)
    {
        _result = user;
    }
}

The event argument class derives from the GetUserCompletedEventArgs class and holds event-related information. We use this to identify the operation status and retrieve the results. In addition to that, we use the RaiseExceptionIfNecessary() method of AsyncCompletedEventArgs class to raise an exception if the operation failed or was canceled.

After that, we create the EapUserProvider class with EAP implementation:

public class EapUserProvider
{
    private readonly SendOrPostCallback _operationFinished;
    private readonly UserService _userService;

    public EapUserProvider()
    {
        _operationFinished = ProcessOperationFinished;
        _userService = new UserService();
    }

    public User GetUser(int userId) => _userService.GetUser(userId);

    public event EventHandler<GetUserCompletedEventArgs> GetUserCompleted;

    public void GetUserAsync(int userId) => GetUserAsync(userId, null);

    private void ProcessOperationFinished(object state)
    {
        var args = (GetUserCompletedEventArgs)state;

        GetUserCompleted?.Invoke(this, args);
    }
}

The _operationFinished field is a SendOrPostCallback delegate that represents a callback method that we want to execute when a message dispatches to a synchronization context.

We assign ProcessOperationFinished() the  _operationFinished delegate. So, ProcessOperationFinished() fires once the task finishes and the call returns to the current synchronization context.

The GetUserCompleted property represents the event that fires on operation completion. This provides the option for consumers to subscribe to this operation.

Then, we add the GetUserAsync() method to the EapUserProvider class and fetch the user asynchronously:

public void GetUserAsync(int userId, object userState)
{
    AsyncOperation operation = AsyncOperationManager.CreateOperation(userState);

    ThreadPool.QueueUserWorkItem(state =>
    {
        GetUserCompletedEventArgs args;
        try
        {
            var user = GetUser(userId);
            args = new GetUserCompletedEventArgs(null, false, user);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            args = new GetUserCompletedEventArgs(e, false, null);
        }

        operation.PostOperationCompleted(_operationFinished, args);

    }, userState);
}

The GetUserAsync() method encapsulates the EAP implementation logic.

First, we create an AsyncOperation object to track and report the progress of the asynchronous task. Thereupon, we run the operation asynchronously on a thread pool using the ThreadPool.QueueWorkItem() method. Then we notify about the operation completion by invoking the PostOperationCompleted() method from the AsyncOperation object. This in turn fires the GetUserCompleted event.

Next, let’s create a EventBasedAsyncPatternHelper class and add the FetchAndPrintUser() method to perform the asynchronous operation using EAP:

public static class EventBasedAsyncPatternHelper
{
    public static void FetchAndPrintUser(int userId)
    {
        var eapUserProvider = new EapUserProvider();

        eapUserProvider.GetUserCompleted += (sender, args) =>
        {
            var result = args.Result;
            Console.WriteLine($"Id: {result.Id}\nName: {result.Name}");
        };

        eapUserProvider.GetUserAsync(userId);
    }
}

We register for the GetUserCompleted event by defining our callback method that prints user information. Then, we start the asynchronous operation by invoking the GetUserAsync() method.

Finally, let’s call the FetchAndPrintUser() method to retrieve and print the user information asynchronously:

EventBasedAsyncPatternHelper.FetchAndPrintUser(100);

Task-Based Asynchronous Pattern (TAP)

TAP is the recommended pattern of implementing asynchronous operations in .NET for new development. This pattern emerged from the Task Parallel Library introduced in .NET Framework 4.0.

TAP uses async and await keywords to implement the pattern. The compiler creates a state machine for methods with async keywords to handle the asynchronous execution.

Meanwhile, the await keyword pauses the execution and asynchronously waits for the awaited Task to complete. At this point, the current thread releases to the thread pool as it unblocks and picks up another task.

To know more about task-based async programming, check out our article on executing multiple tasks asynchronously.

Asynchronous Programming Implementation

We can implement TAP with or without the async keyword:

public Task<int> OperationName1Async(int param)
{
    // more code
    return Task.FromResult(1);
}

public async Task<int> OperationName2Async(int param)
{
    // more code with await
    return 1;
}

With TAP, we define an asynchronous method that normally returns Task or Task<T>. In the first example, OperationName1Async() returns a Task object. This encapsulates the whole asynchronous lifecycle and it’s our responsibility to manage it manually to create, run and close the task.

On the other hand, the OperationName2Async() method is using the async keyword. It’s the simplest version of implementing asynchronous patterns in .NET to date. The compiler generates methods to manage the Task lifecycle and we only need to manage it by using the async and await keywords.

CancellationToken

An important advantage of using tasks is the ability to cancel the task at any time. For example, suppose a user navigates from one web page to another, while a long-running operation is in progress. In that case, it makes sense to cancel the operation from the previous page if it is no longer needed.

We can achieve this using the CancellationToken mechanism of Task. Basically, a CancellationToken is a carrier of cancellation requests. It’s an optional parameter that we can pass to asynchronous operations. The asynchronous operations monitor this token for any cancellation requests and abort accordingly.

Here is a simple example that uses a cancellation token to abort the task:

var cancelToken = new CancellationTokenSource();
Task.Factory.StartNew(async () =>
{
    await Task.Delay(3000, cancelToken.Token);
    // API call
}, cancelToken.Token);

//Stops the task
cancelToken.Cancel(false);

The Cancel() method of the CancellationTokenSource class notifies of a cancellation request and all the tasks that use the token will abort.

Check out our article Canceling HTTP Requests in ASP.NET Core, where we discuss more about CancellationToken.

Let’s see TAP in action by implementing the same user fetch asynchronous operation we did for EAP. But this time, using TAP.

TAP Code Example

We start by defining a TapUserProvider class with TAP implementation:

public class TapUserProvider
{
    private readonly UserService _userService;

    public TapUserProvider()
    {
        _userService = new UserService();
    }

    public User GetUser(int userId) => _userService.GetUser(userId);

    public Task<User> GetUserAsync(int userId)
    {
        return Task.Run(() => GetUser(userId));
    }
}

The GetUserAsync() method spins up a thread using the Task.Run() method and calls the user service to fetch the user information.

After that, we create a TaskBasedAsyncPatternHelper class with FetchAndPrintUser() method to asynchronously call the GetUserAsync() method using async and await keywords:

public static class TaskBasedAsyncPatternHelper
{
    public static async Task FetchAndPrintUser(int userId)
    {
        var tapUserProvider = new TapUserProvider();

        var user = await tapUserProvider.GetUserAsync(userId);

        Console.WriteLine($"Id: {user.Id}\nName: {user.Name}");
    }
}

As the GetUserAsync() method returns a Task, we use await keyword to wait for the execution to finish. Post completion, the code after await keyword executes and prints the user information.

Finally, let’s invoke the FetchAndPrintUser() method to retrieve and print user information:

await TaskBasedAsyncPatternHelper.FetchAndPrintUser(100);

Conclusion

To sum it up, we have learned about asynchronous programming and how it has evolved in .NET from APM to TAP using practical examples.

TAP is the recommended approach in .NET to implement async programming, while Microsoft considers APM and EAP as legacy models. It is also easier to implement TAP since it only uses a single method to represent the initiation and completion of an asynchronous operation.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!