Asynchronous Lambdas and Local Functions

Just as a lambda expression converted to a delegate can be used as a concise syntax for declaring a normal method, lambdas containing await expressions can be converted to delegates14. To do so, just precede the lambda expression with the async keyword. In Listing 20.10, we first assign an async lambdato a Func<string, Task> writeWebRequestSizeAsync variable. We then use the await operator to invoke it.

Listing 20.10: An Asynchronous Client-Server Interaction as a Lambda Expression
using System;
using System.IO;
using System.Net;
using System.Linq;
using System.Threading.Tasks;
 
public class Program
{
 
    public static void Main(string[] args)
    {
        string url = "http://www.IntelliTect.com";
        if(args.Length > 0)
        {
            url = args[0];
        }
 
        Console.Write(url);
 
        Func<string, Task> writeWebRequestSizeAsync =
            async (string webRequestUrl) =>
            {
                // Error handling omitted for 
                // elucidation
                WebRequest webRequest =
                   WebRequest.Create(url);
 
                WebResponse response =
                    await webRequest.GetResponseAsync();
 
                // Explicitly counting rather than invoking
                // webRequest.ContentLength to demonstrate
                //  multiple await operators
                using (StreamReader reader =
                    new StreamReader(
                        response.GetResponseStream()))
                {
                    string text =
                        (await reader.ReadToEndAsync());
                    Console.WriteLine(
                        FormatBytes(text.Length));
                }
            };
 
        Task task = writeWebRequestSizeAsync(url);
 
        while (!task.Wait(100))
        {
            Console.Write(".");
        }
    }
    // ...
}

Similarly, the same effect can be achieved with a local function15. For example, in Listing 20.10, you could change the lambda expression header (everything up to and including the => operator) to

async Task WriteWebRequestSizeAsync(string webRequestUrl)

leaving everything in the body, including the curly braces, unchanged.

Note that an async lambda expression has the same restrictions as the named async method:

AdVanced/Beginner Topic
Implementing a Custom Asynchronous Method

Implementing an asynchronous method by relying on other asynchronous methods (which, in turn, rely on more asynchronous methods) is relatively easy with the await keyword. However, at some point in the call hierarchy, it becomes necessary to write a “leaf” asynchronous Task-returning method. Consider, for example, an asynchronous method for running a command-line program with the eventual goal that the output could be accessed. Such a method would be declared as follows:

public static Task<Process> RunProcessAsync(string filename)

The simplest implementation would, of course, be to rely on Task.Run() again and call both the System.Diagnostics.Process’s Start() and WaitForExit() methods. However, creating an additional thread in the current process is unnecessary when the invoked process itself will have its own collection of one or more threads. To implement the RunProcessAsync() method and return to the caller’s synchronization context when the invoked process completes, we can rely on a TaskCompletionSource<T> object, as shown in Listing 20.11.

Listing 20.11: Implementing a Custom Asynchronous Method
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
public class Program
{
    public static Task<Process> RunProcessAsync(
        string fileName,
        string arguments = "",
        CancellationToken cancellationToken = default)
    {
        TaskCompletionSource<Process> taskCS =
                      new TaskCompletionSource<Process>();
 
        Process process = new Process()
        {
            StartInfo = new ProcessStartInfo(fileName)
            {
                UseShellExecute = false,
                Arguments = arguments
            },
            EnableRaisingEvents = true
        };
 
        process.Exited += (sender, localEventArgs) =>
        {
            taskCS.SetResult(process);
        };
 
        cancellationToken
            .ThrowIfCancellationRequested();
 
        process.Start();
 
        cancellationToken.Register(() =>
        {
            Process.GetProcessById(process.Id).Kill();
        });
 
        return taskCS.Task;
    }
    // ...
}

Ignore the highlighting for the moment and instead focus on the pattern of using an event for notification when the process completes. Since System.Diagnostics.Process includes a notification upon exit, we register for this notification and use it as a callback from which we can invoke TaskCompletionSource.SetResult(). The code in Listing 20.11 follows a fairly common pattern that you can use to create an asynchronous method without having to resort to Task.Run().

Another important characteristic that an async method might require is cancellation. TAP relies on the same methods for cancellation as the TPL does—namely, a System.Threading.CancellationToken. Listing 20.11 highlights the code necessary to support cancellation. In this example, we allow for canceling before the process ever starts, as well as an attempt to close the application’s main window (if there is one). A more aggressive approach would be to call Process.Kill(), but this method could potentially cause problems for the program that is executing.

Notice that we don’t register for the cancellation event until after the process is started. This avoids any race conditions that might occur if cancellation is triggered before the process actually begins.

One last feature to consider supporting is a progress update. Listing 20.12 is the full version of RunProcessAsync() with just such an update.

Listing 20.12: Implementing a Custom Asynchronous Method with Progress Support
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
public class Program
{
    public static Task<Process> RunProcessAsync(
        string fileName,
        string arguments = "",
        IProgress<ProcessProgressEventArgs>? progress = 
            nullobject? objectState = null,
        CancellationToken cancellationToken =
            default(CancellationToken))
    {
        TaskCompletionSource<Process> taskCS =
                      new TaskCompletionSource<Process>();
 
        Process process = new Process()
        {
            StartInfo = new ProcessStartInfo(fileName)
            {
                UseShellExecute = false,
                Arguments = arguments,
                RedirectStandardOutput =
                   progress != null
            },
            EnableRaisingEvents = true
        };
 
        process.Exited += (sender, localEventArgs) =>
        {
            taskCS.SetResult(process);
        };
 
        if (progress != null)
        {
            process.OutputDataReceived +=
                (sender, localEventArgs) =>
            {
                progress.Report(
                    new ProcessProgressEventArgs(
                        localEventArgs.Data,
                        objectState));
            };
        }
 
        cancellationToken
            .ThrowIfCancellationRequested();
 
        process.Start();
 
        if (progress !=null)
        {
            process.BeginOutputReadLine();
        }
 
        cancellationToken.Register(() =>
        {
            Process.GetProcessById(process.Id).Kill();
        });
 
        return taskCS.Task;
    }
 
    // ...
}
 
public class ProcessProgressEventArgs
{
    // ...
}

Wrapping your head around precisely what is happening in an async method can be difficult, but it is far less difficult than trying to figure out what asynchronous code written with explicit continuations in lambdas is doing. The key points to remember are as follows:

________________________________________

14. Starting in C# 5.0.
15. Starting in C# 7.0.
16. Technically, it is an awaitable type, as described in the Advanced Topic: Valid Async Return Types Expanded.”
{{ snackbarMessage }}