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.
Let’s start by understanding what asynchronous programming is.
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
ofAsyncCallback
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.