IAsyncDisposable is the asynchronous equivalent of IDisposable, so it can be invoked using C# 8.0’s new await using statement or await using declaration. In Listing 20.6, we use the latter when declaring outputFileStream because, like IAsyncEnumerable<T>, FileStream also implements IAsyncDisposable. As with the using declarative, you can’t reassign a variable declared with async using.
Not surprisingly, the await using statement follows the same syntax as the common using statement:
await using FileStream outputFileStream =
new FileStream(encryptedFileName, FileMode.Create);
{ ... }
Both can be used anytime the type implements IAsyncDisposable or simply has a DisposeAsync() method. The result is that the C# compiler injects a try/finally block around the declaration and before the variable goes out of scope, and then it invokes await DisposeAsync() within the finally block.11 This approach ensures that all resources are cleaned up.
Note that IAsyncDisposable and IDisposable are not related to each other via inheritance. In consequence, their implementations are not dependent, either: One can be implemented without the other.
In the await foreach statement of Listing 20.6, we invoke the LINQ AsyncEnumberable.Zip() method to pair the original filename with the encrypted filename.
await foreach (
(string fileName, string encryptedFileName) in
EncryptFilesAsync(files)
.Zip(files.ToAsyncEnumerable()))
{
Console.WriteLine($"{fileName}=>{encryptedFileName}");
}
AsyncEnumerable provides the LINQ functionality for IAsyncEnumerable<T>, as you might expect. However, the library is not available in the BCL.12 Instead, to access the asynchronous LINQ capabilities, you need to add a reference to the System.Linq.Async NuGet package.
AsyncEnumerable is defined in System.Linq (not a different unique namespace with async functionality). Not surprisingly, it includes asynchronous versions of the standard LINQ operators such as Where(), Select(), and the Zip() method used in the aforementioned listing. They are considered “asynchronous versions” because they are extension methods on IAsyncEnumerable rather than IEnumerable<T>. In addition, AsyncEnumerable includes a series of *Async(), *AwaitAsync(), and *AwaitWithCancellationAsync() methods. The Select*() versions of each of these methods are shown in Listing 20.8.
The method name that matches the Enumerable equivalent—Select() in this case—has a similar “instance” signature, but the TResult and TSource are different. Both signatures with “Await” in the name take asynchronous signatures that include a selector that returns a ValueTask<T>. For example, you could invoke Listing 20.6’s EncryptFileAsync() method from SelectAwait() as follows:
IAsyncEnumerable<string> items = files.ToAsyncEnumerable();
items = items.SelectAwait(
(text, id) => EncryptFileAsync(text));
The important thing to note is that EncyryptFileAsync() method returns a ValueTask<T>, which is what both *Await() and *AwaitWithCancellationAsync() require. The latter, of course, also allows for specification of a cancellation token.
Another asynchronous LINQ method worthy of mention is the ToAsyncEnumerable() method used in Listing 20.6. Since asynchronous LINQ methods work with IAsyncEnumerable<T> interfaces, ToAsyncEnumerable() takes care of converting IEnumerable<T> to IAsyncEnumerable<T>. Similarly, a ToEnumerable() method makes the opposite conversion. (Admittedly, using files.ToAsyncEnumerable() in the snippet is a contrived example for retrieving an IAsyncEnumerable<string>.)
The scalar versions of the asynchronous LINQ methods similarly match the IEnumerable<T>—with a *Await(), *AwaitAsync(), and *AwaitWithCancellation() set of members. The key difference is that they all return a ValueTask<T>. The following snippet provides an example of using the AverageAsync() method:
double average = await AsyncEnumerable.Range(
0, 999).AverageAsync();
As such, we can use await to treat the return as a double rather than a ValueTask<double>.
________________________________________