.NET Continuations
In asynchronous programming with .NET, continuations are operations that execute after a previous asynchronous operation completes. They allow you to create chains of operations where each step depends on the result of the previous one. Continuations are a fundamental concept that helps make asynchronous code more readable and maintainable.
What Are Continuations?
A continuation is code that runs after an asynchronous task completes, either successfully or with an error. In .NET, continuations are primarily implemented using methods like ContinueWith
on the Task
class or more commonly with the await
keyword.
Think of continuations as "what happens next" after an asynchronous operation finishes. They help you:
- Chain asynchronous operations sequentially
- Handle success and error conditions
- Transform the results of asynchronous operations
- Coordinate multiple asynchronous operations
Basic Continuations with ContinueWith
The most explicit way to create a continuation is using the ContinueWith
method on the Task
class.
public async Task DemonstrateContinueWith()
{
Console.WriteLine("Starting the operation...");
Task<int> task = Task.Run(() => {
Console.WriteLine("Performing calculation...");
return 42;
});
Task<string> continuation = task.ContinueWith(previousTask => {
Console.WriteLine("Continuation executing...");
return $"The result is: {previousTask.Result}";
});
string result = await continuation;
Console.WriteLine(result);
}
Output:
Starting the operation...
Performing calculation...
Continuation executing...
The result is: 42
In this example:
- We create a task that computes a value (42)
- We attach a continuation that takes the result and formats it as a string
- We await the final result
Handling Different Completion States
ContinueWith
allows you to specify continuations that execute based on how the task completed:
public async Task HandleDifferentCompletionStates()
{
Task<int> task = Task.Run(() => {
if (DateTime.Now.Millisecond % 2 == 0)
return 42;
else
throw new Exception("Something went wrong!");
});
// Executes only if the task completed successfully
Task successContinuation = task.ContinueWith(
t => Console.WriteLine($"Success! Result: {t.Result}"),
TaskContinuationOptions.OnlyOnRanToCompletion);
// Executes only if the task faulted
Task failureContinuation = task.ContinueWith(
t => Console.WriteLine($"Failed: {t.Exception.InnerException.Message}"),
TaskContinuationOptions.OnlyOnFaulted);
await Task.WhenAll(successContinuation, failureContinuation);
}
Depending on whether the random condition is met, you'll see either:
Success! Result: 42
Or:
Failed: Something went wrong!
Modern Continuations with async
/await
While ContinueWith
gives you explicit control over continuations, the async
/await
pattern provides a much more readable way to work with continuations:
public async Task ModernContinuationsExample()
{
try
{
Console.WriteLine("Starting operation...");
// First asynchronous operation
int result = await CalculateValueAsync();
// This is a continuation that runs after CalculateValueAsync completes
string processed = await ProcessResultAsync(result);
// Another continuation
await DisplayResultAsync(processed);
}
catch (Exception ex)
{
Console.WriteLine($"Error occurred: {ex.Message}");
}
}
private async Task<int> CalculateValueAsync()
{
await Task.Delay(1000); // Simulating work
Console.WriteLine("Calculation complete");
return 42;
}
private async Task<string> ProcessResultAsync(int value)
{
await Task.Delay(500); // Simulating work
Console.WriteLine("Processing complete");
return $"Processed: {value * 2}";
}
private async Task DisplayResultAsync(string message)
{
await Task.Delay(300); // Simulating work
Console.WriteLine($"Display: {message}");
}
Output:
Starting operation...
Calculation complete
Processing complete
Display: Processed: 84
With async
/await
, the continuation logic is implicit. Each await
effectively creates a continuation that resumes when the awaited task completes.
Multiple Continuations
You can create multiple independent continuations from a single task:
public async Task MultipleContinuations()
{
Task<int> initialTask = Task.Run(async () => {
await Task.Delay(1000);
return 100;
});
// Create three different continuations from the same task
Task<int> continuation1 = initialTask.ContinueWith(t => t.Result * 2);
Task<int> continuation2 = initialTask.ContinueWith(t => t.Result + 50);
Task<string> continuation3 = initialTask.ContinueWith(t => $"Result was: {t.Result}");
// Wait for all continuations to complete
await Task.WhenAll(continuation1, continuation2, continuation3);
Console.WriteLine($"Continuation 1: {await continuation1}");
Console.WriteLine($"Continuation 2: {await continuation2}");
Console.WriteLine($"Continuation 3: {await continuation3}");
}
Output:
Continuation 1: 200
Continuation 2: 150
Continuation 3: Result was: 100
Continuation Chaining
Continuations can be chained together to create sequences of operations:
public async Task ChainedContinuations()
{
var result = await Task.Run(() => 10)
// First continuation
.ContinueWith(t => {
Console.WriteLine($"First continuation with: {t.Result}");
return t.Result * 2;
})
// Second continuation
.ContinueWith(t => {
Console.WriteLine($"Second continuation with: {t.Result}");
return t.Result + 5;
})
// Third continuation
.ContinueWith(t => {
Console.WriteLine($"Third continuation with: {t.Result}");
return $"Final result: {t.Result}";
});
Console.WriteLine(result);
}
Output:
First continuation with: 10
Second continuation with: 20
Third continuation with: 25
Final result: 25
Real-World Example: Web API Request Chain
Let's look at a practical example where continuations help with a sequence of web API calls:
public async Task<UserReportViewModel> GenerateUserReport(int userId)
{
// First task: get user profile
UserProfile user = await GetUserProfileAsync(userId);
// Continuation: get user's recent activity
// (This only executes after we have the user profile)
List<Activity> recentActivity = await GetUserRecentActivityAsync(user.Username);
// Continuation: get recommendations based on activity
// (This only executes after we have the recent activity)
List<Recommendation> recommendations = await GetRecommendationsAsync(recentActivity);
// Final continuation: build the report view model
return new UserReportViewModel
{
UserProfile = user,
RecentActivities = recentActivity,
Recommendations = recommendations,
GeneratedDate = DateTime.Now
};
}
private async Task<UserProfile> GetUserProfileAsync(int userId)
{
using var client = new HttpClient();
var response = await client.GetAsync($"https://api.example.com/users/{userId}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<UserProfile>(json);
}
private async Task<List<Activity>> GetUserRecentActivityAsync(string username)
{
// Implementation details omitted for brevity
await Task.Delay(500); // Simulating API call
return new List<Activity>();
}
private async Task<List<Recommendation>> GetRecommendationsAsync(List<Activity> activities)
{
// Implementation details omitted for brevity
await Task.Delay(700); // Simulating API call
return new List<Recommendation>();
}
This example demonstrates how continuations create a clear sequence of dependent operations. Each step depends on data from the previous step, and the code reads almost like synchronous code while maintaining the benefits of asynchronous execution.
Best Practices for Continuations
-
Prefer
async
/await
overContinueWith
: Theasync
/await
pattern is more readable and handles exceptions better. -
Be careful with
Task.Result
and.Wait()
: Using these can lead to deadlocks. Instead, useawait
to get results asynchronously. -
Handle exceptions properly: Use try/catch blocks or continuation options to handle errors.
-
Consider using
ConfigureAwait(false)
: In library code, useConfigureAwait(false)
to avoid potential deadlocks:
// In library code
public async Task<int> LibraryMethodAsync()
{
// This won't force continuation on the original context
int result = await SomeOperationAsync().ConfigureAwait(false);
return result * 2;
}
- Use
TaskContinuationOptions
appropriately: When usingContinueWith
, specify the right options for execution conditions.
Common Continuation Options
When using ContinueWith
, you can specify various options:
public async Task ContinuationOptionsExample()
{
var task = Task.Run(() => {
Console.WriteLine("Original task running");
return 42;
});
// Run only if the previous task completed successfully
task.ContinueWith(t => Console.WriteLine("Success!"),
TaskContinuationOptions.OnlyOnRanToCompletion);
// Run only if the previous task threw an exception
task.ContinueWith(t => Console.WriteLine("Failed!"),
TaskContinuationOptions.OnlyOnFaulted);
// Run with a specific scheduling priority
task.ContinueWith(t => Console.WriteLine("High priority continuation"),
TaskContinuationOptions.HideScheduler);
// Wait for the original task to complete
await task;
}
Summary
Continuations in .NET provide a powerful way to handle the completion of asynchronous operations. They allow you to:
- Chain multiple asynchronous operations together
- React differently to success and failure conditions
- Transform the results of asynchronous operations
- Create more readable and maintainable asynchronous code
While the explicit ContinueWith
method gives you fine-grained control, the async
/await
pattern provides a more intuitive way to work with continuations in most cases. By understanding continuations, you'll be able to write cleaner asynchronous code that is easier to reason about.
Exercises
-
Create a chain of three asynchronous operations using
async
/await
where each operation depends on the result of the previous one. -
Implement error handling in a continuation chain that gracefully recovers from exceptions.
-
Write a method that starts five parallel tasks and then creates a continuation that executes when all of them complete.
-
Build a simple web scraper that downloads a web page, then continues by extracting all links, and then continues by checking if each link is valid.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)