// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq.Expressions; using System.Reflection; namespace System.Threading.Tasks { internal static class TaskHelpersExtensions { private static Task _defaultCompleted = TaskHelpers.FromResult(default(AsyncVoid)); private static readonly Action _rethrowWithNoStackLossDelegate = GetRethrowWithNoStackLossDelegate(); /// /// Calls the given continuation, after the given task completes, if it ends in a faulted state. /// Will not be called if the task did not fault (meaning, it will not be called if the task ran /// to completion or was canceled). Intended to roughly emulate C# 5's support for "try/catch" in /// async methods. Note that this method allows you to return a Task, so that you can either return /// a completed Task (indicating that you swallowed the exception) or a faulted task (indicating that /// that the exception should be propagated). In C#, you cannot normally use await within a catch /// block, so returning a real async task should never be done from Catch(). /// internal static Task Catch(this Task task, Func continuation, CancellationToken cancellationToken = default(CancellationToken)) { // Fast path for successful tasks, to prevent an extra TCS allocation if (task.Status == TaskStatus.RanToCompletion) { return task; } return task.CatchImpl(() => continuation(new CatchInfo(task)).Task.ToTask(), cancellationToken); } /// /// Calls the given continuation, after the given task completes, if it ends in a faulted state. /// Will not be called if the task did not fault (meaning, it will not be called if the task ran /// to completion or was canceled). Intended to roughly emulate C# 5's support for "try/catch" in /// async methods. Note that this method allows you to return a Task, so that you can either return /// a completed Task (indicating that you swallowed the exception) or a faulted task (indicating that /// that the exception should be propagated). In C#, you cannot normally use await within a catch /// block, so returning a real async task should never be done from Catch(). /// internal static Task Catch(this Task task, Func, CatchInfo.CatchResult> continuation, CancellationToken cancellationToken = default(CancellationToken)) { // Fast path for successful tasks, to prevent an extra TCS allocation if (task.Status == TaskStatus.RanToCompletion) { return task; } return task.CatchImpl(() => continuation(new CatchInfo(task)).Task, cancellationToken); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "CatchInfo", Justification = "This is the name of a class.")] [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "TaskHelpersExtensions", Justification = "This is the name of a class.")] private static Task CatchImpl(this Task task, Func> continuation, CancellationToken cancellationToken) { // Stay on the same thread if we can if (task.IsCompleted) { if (task.IsFaulted) { try { Task resultTask = continuation(); if (resultTask == null) { // Not a resource because this is an internal class, and this is a guard clause that's intended // to be thrown by us to us, never escaping out to end users. throw new InvalidOperationException("You must set the Task property of the CatchInfo returned from the TaskHelpersExtensions.Catch continuation."); } return resultTask; } catch (Exception ex) { return TaskHelpers.FromError(ex); } } if (task.IsCanceled || cancellationToken.IsCancellationRequested) { return TaskHelpers.Canceled(); } if (task.Status == TaskStatus.RanToCompletion) { TaskCompletionSource tcs = new TaskCompletionSource(); tcs.TrySetFromTask(task); return tcs.Task; } } // Split into a continuation method so that we don't create a closure unnecessarily return CatchImplContinuation(task, continuation); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "TaskHelpersExtensions", Justification = "This is the name of a class.")] [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] private static Task CatchImplContinuation(Task task, Func> continuation) { SynchronizationContext syncContext = SynchronizationContext.Current; return task.ContinueWith(innerTask => { TaskCompletionSource> tcs = new TaskCompletionSource>(); if (innerTask.IsFaulted) { if (syncContext != null) { syncContext.Post(state => { try { Task resultTask = continuation(); if (resultTask == null) { throw new InvalidOperationException("You cannot return null from the TaskHelpersExtensions.Catch continuation. You must return a valid task or throw an exception."); } tcs.TrySetResult(resultTask); } catch (Exception ex) { tcs.TrySetException(ex); } }, state: null); } else { Task resultTask = continuation(); if (resultTask == null) { throw new InvalidOperationException("You cannot return null from the TaskHelpersExtensions.Catch continuation. You must return a valid task or throw an exception."); } tcs.TrySetResult(resultTask); } } else { tcs.TrySetFromTask(innerTask); } return tcs.Task.FastUnwrap(); }).FastUnwrap(); } /// /// Upon completion of the task, copies its result into the given task completion source, regardless of the /// completion state. This causes the original task to be fully observed, and the task that is returned by /// this method will always successfully run to completion, regardless of the original task state. /// Since this method consumes a task with no return value, you must provide the return value to be used /// when the inner task ran to successful completion. /// internal static Task CopyResultToCompletionSource(this Task task, TaskCompletionSource tcs, TResult completionResult) { return task.CopyResultToCompletionSourceImpl(tcs, innerTask => completionResult); } /// /// Upon completion of the task, copies its result into the given task completion source, regardless of the /// completion state. This causes the original task to be fully observed, and the task that is returned by /// this method will always successfully run to completion, regardless of the original task state. /// [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] internal static Task CopyResultToCompletionSource(this Task task, TaskCompletionSource tcs) { return task.CopyResultToCompletionSourceImpl(tcs, innerTask => innerTask.Result); } private static Task CopyResultToCompletionSourceImpl(this TTask task, TaskCompletionSource tcs, Func resultThunk) where TTask : Task { // Stay on the same thread if we can if (task.IsCompleted) { switch (task.Status) { case TaskStatus.Canceled: case TaskStatus.Faulted: TaskHelpers.TrySetFromTask(tcs, task); break; case TaskStatus.RanToCompletion: tcs.TrySetResult(resultThunk(task)); break; } return TaskHelpers.Completed(); } // Split into a continuation method so that we don't create a closure unnecessarily return CopyResultToCompletionSourceImplContinuation(task, tcs, resultThunk); } [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] private static Task CopyResultToCompletionSourceImplContinuation(TTask task, TaskCompletionSource tcs, Func resultThunk) where TTask : Task { return task.ContinueWith(innerTask => { switch (innerTask.Status) { case TaskStatus.Canceled: case TaskStatus.Faulted: TaskHelpers.TrySetFromTask(tcs, innerTask); break; case TaskStatus.RanToCompletion: tcs.TrySetResult(resultThunk(task)); break; } }, TaskContinuationOptions.ExecuteSynchronously); } /// /// A version of task.Unwrap that is optimized to prevent unnecessarily capturing the /// execution context when the antecedent task is already completed. /// [SuppressMessage("Microsoft.WebAPI", "CR4000:DoNotUseProblematicTaskTypes", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] internal static Task FastUnwrap(this Task task) { Task innerTask = task.Status == TaskStatus.RanToCompletion ? task.Result : null; return innerTask ?? task.Unwrap(); } /// /// A version of task.Unwrap that is optimized to prevent unnecessarily capturing the /// execution context when the antecedent task is already completed. /// [SuppressMessage("Microsoft.WebAPI", "CR4000:DoNotUseProblematicTaskTypes", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] internal static Task FastUnwrap(this Task> task) { Task innerTask = task.Status == TaskStatus.RanToCompletion ? task.Result : null; return innerTask ?? task.Unwrap(); } /// /// Calls the given continuation, after the given task has completed, regardless of the state /// the task ended in. Intended to roughly emulate C# 5's support for "finally" in async methods. /// [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] internal static Task Finally(this Task task, Action continuation) { // Stay on the same thread if we can if (task.IsCompleted) { try { continuation(); return task; } catch (Exception ex) { MarkExceptionsObserved(task); return TaskHelpers.FromError(ex); } } // Split into a continuation method so that we don't create a closure unnecessarily return FinallyImplContinuation(task, continuation); } /// /// Calls the given continuation, after the given task has completed, regardless of the state /// the task ended in. Intended to roughly emulate C# 5's support for "finally" in async methods. /// [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] internal static Task Finally(this Task task, Action continuation) { // Stay on the same thread if we can if (task.IsCompleted) { try { continuation(); return task; } catch (Exception ex) { MarkExceptionsObserved(task); return TaskHelpers.FromError(ex); } } // Split into a continuation method so that we don't create a closure unnecessarily return FinallyImplContinuation(task, continuation); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] private static Task FinallyImplContinuation(Task task, Action continuation) { SynchronizationContext syncContext = SynchronizationContext.Current; return task.ContinueWith(innerTask => { TaskCompletionSource tcs = new TaskCompletionSource(); if (syncContext != null) { syncContext.Post(state => { try { continuation(); tcs.TrySetFromTask(innerTask); } catch (Exception ex) { MarkExceptionsObserved(innerTask); tcs.SetException(ex); } }, state: null); } else { try { continuation(); tcs.TrySetFromTask(innerTask); } catch (Exception ex) { MarkExceptionsObserved(innerTask); tcs.SetException(ex); } } return tcs.Task; }).FastUnwrap(); } [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification = "This general exception is not intended to be seen by the user")] [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This general exception is not intended to be seen by the user")] [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] private static Action GetRethrowWithNoStackLossDelegate() { MethodInfo getAwaiterMethod = typeof(Task).GetMethod("GetAwaiter", Type.EmptyTypes); if (getAwaiterMethod != null) { // .NET 4.5 - dump the same code the 'await' keyword would have dumped // >> task.GetAwaiter().GetResult() // No-ops if the task completed successfully, else throws the originating exception complete with the correct call stack. var taskParameter = Expression.Parameter(typeof(Task)); var getAwaiterCall = Expression.Call(taskParameter, getAwaiterMethod); var getResultCall = Expression.Call(getAwaiterCall, "GetResult", Type.EmptyTypes); var lambda = Expression.Lambda>(getResultCall, taskParameter); return lambda.Compile(); } else { Func prepForRemoting = null; try { if (AppDomain.CurrentDomain.IsFullyTrusted) { // .NET 4 - do the same thing Lazy does by calling Exception.PrepForRemoting // This is an internal method in mscorlib.dll, so pass a test Exception to it to make sure we can call it. var exceptionParameter = Expression.Parameter(typeof(Exception)); var prepForRemotingCall = Expression.Call(exceptionParameter, "PrepForRemoting", Type.EmptyTypes); var lambda = Expression.Lambda>(prepForRemotingCall, exceptionParameter); var func = lambda.Compile(); func(new Exception()); // make sure the method call succeeds before assigning the 'prepForRemoting' local variable prepForRemoting = func; } } catch { } // If delegate creation fails (medium trust) we will simply throw the base exception. return task => { try { task.Wait(); } catch (AggregateException ex) { Exception baseException = ex.GetBaseException(); if (prepForRemoting != null) { baseException = prepForRemoting(baseException); } throw baseException; } }; } } /// /// Marks a Task as "exception observed". The Task is required to have been completed first. /// /// /// Useful for 'finally' clauses, as if the 'finally' action throws we'll propagate the new /// exception and lose track of the inner exception. /// [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "unused", Justification = "We only call the property getter for its side effect; we don't care about the value.")] private static void MarkExceptionsObserved(this Task task) { Contract.Assert(task.IsCompleted); Exception unused = task.Exception; } /// /// Calls the given continuation, after the given task has completed, if the task successfully ran /// to completion (i.e., was not cancelled and did not fault). /// internal static Task Then(this Task task, Action continuation, CancellationToken cancellationToken = default(CancellationToken)) { return task.ThenImpl(t => ToAsyncVoidTask(continuation), cancellationToken); } /// /// Calls the given continuation, after the given task has completed, if the task successfully ran /// to completion (i.e., was not cancelled and did not fault). /// internal static Task Then(this Task task, Func continuation, CancellationToken cancellationToken = default(CancellationToken)) { return task.ThenImpl(t => TaskHelpers.FromResult(continuation()), cancellationToken); } /// /// Calls the given continuation, after the given task has completed, if the task successfully ran /// to completion (i.e., was not cancelled and did not fault). /// internal static Task Then(this Task task, Func continuation, CancellationToken cancellationToken = default(CancellationToken)) { return task.Then(() => continuation().Then(() => default(AsyncVoid)), cancellationToken); } /// /// Calls the given continuation, after the given task has completed, if the task successfully ran /// to completion (i.e., was not cancelled and did not fault). /// internal static Task Then(this Task task, Func> continuation, CancellationToken cancellationToken = default(CancellationToken)) { return task.ThenImpl(t => continuation(), cancellationToken); } /// /// Calls the given continuation, after the given task has completed, if the task successfully ran /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the /// result of the task as its sole parameter. /// [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] internal static Task Then(this Task task, Action continuation, CancellationToken cancellationToken = default(CancellationToken)) { return task.ThenImpl(t => ToAsyncVoidTask(() => continuation(t.Result)), cancellationToken); } /// /// Calls the given continuation, after the given task has completed, if the task successfully ran /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the /// result of the task as its sole parameter. /// [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] internal static Task Then(this Task task, Func continuation, CancellationToken cancellationToken = default(CancellationToken)) { return task.ThenImpl(t => TaskHelpers.FromResult(continuation(t.Result)), cancellationToken); } /// /// Calls the given continuation, after the given task has completed, if the task successfully ran /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the /// result of the task as its sole parameter. /// [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] internal static Task Then(this Task task, Func> continuation, CancellationToken cancellationToken = default(CancellationToken)) { return task.ThenImpl(t => continuation(t.Result), cancellationToken); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] private static Task ThenImpl(this TTask task, Func> continuation, CancellationToken cancellationToken) where TTask : Task { // Stay on the same thread if we can if (task.IsCompleted) { if (task.IsFaulted) { return TaskHelpers.FromErrors(task.Exception.InnerExceptions); } if (task.IsCanceled || cancellationToken.IsCancellationRequested) { return TaskHelpers.Canceled(); } if (task.Status == TaskStatus.RanToCompletion) { try { return continuation(task); } catch (Exception ex) { return TaskHelpers.FromError(ex); } } } // Split into a continuation method so that we don't create a closure unnecessarily return ThenImplContinuation(task, continuation, cancellationToken); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] private static Task ThenImplContinuation(TTask task, Func> continuation, CancellationToken cancellationToken) where TTask : Task { SynchronizationContext syncContext = SynchronizationContext.Current; return task.ContinueWith(innerTask => { if (innerTask.IsFaulted) { return TaskHelpers.FromErrors(innerTask.Exception.InnerExceptions); } if (innerTask.IsCanceled || cancellationToken.IsCancellationRequested) { return TaskHelpers.Canceled(); } TaskCompletionSource> tcs = new TaskCompletionSource>(); if (syncContext != null) { syncContext.Post(state => { try { tcs.TrySetResult(continuation(task)); } catch (Exception ex) { tcs.TrySetException(ex); } }, state: null); } else { tcs.TrySetResult(continuation(task)); } return tcs.Task.FastUnwrap(); }).FastUnwrap(); } /// /// Throws the first faulting exception for a task which is faulted. It attempts to preserve the original /// stack trace when throwing the exception (which should always work in 4.5, and should also work in 4.0 /// when running in full trust). Note: It is the caller's responsibility not to pass incomplete tasks to /// this method, because it does degenerate into a call to the equivalent of .Wait() on the task when it /// hasn't yet completed. /// internal static void ThrowIfFaulted(this Task task) { _rethrowWithNoStackLossDelegate(task); } /// /// Adapts any action into a Task (returning AsyncVoid, so that it's usable with Task{T} extension methods). /// private static Task ToAsyncVoidTask(Action action) { return TaskHelpers.RunSynchronously(() => { action(); return _defaultCompleted; }); } /// /// Changes the return value of a task to the given result, if the task ends in the RanToCompletion state. /// This potentially imposes an extra ContinueWith to convert a non-completed task, so use this with caution. /// internal static Task ToTask(this Task task, CancellationToken cancellationToken = default(CancellationToken), TResult result = default(TResult)) { if (task == null) { return null; } // Stay on the same thread if we can if (task.IsCompleted) { if (task.IsFaulted) { return TaskHelpers.FromErrors(task.Exception.InnerExceptions); } if (task.IsCanceled || cancellationToken.IsCancellationRequested) { return TaskHelpers.Canceled(); } if (task.Status == TaskStatus.RanToCompletion) { return TaskHelpers.FromResult(result); } } // Split into a continuation method so that we don't create a closure unnecessarily return ToTaskContinuation(task, result); } [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] private static Task ToTaskContinuation(Task task, TResult result) { return task.ContinueWith(innerTask => { TaskCompletionSource tcs = new TaskCompletionSource(); if (task.Status == TaskStatus.RanToCompletion) { tcs.TrySetResult(result); } else { tcs.TrySetFromTask(innerTask); } return tcs.Task; }, TaskContinuationOptions.ExecuteSynchronously).FastUnwrap(); } /// /// Attempts to get the result value for the given task. If the task ran to completion, then /// it will return true and set the result value; otherwise, it will return false. /// [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")] internal static bool TryGetResult(this Task task, out TResult result) { if (task.Status == TaskStatus.RanToCompletion) { result = task.Result; return true; } result = default(TResult); return false; } /// /// Used as the T in a "conversion" of a Task into a Task{T} /// private struct AsyncVoid { } } internal abstract class CatchInfoBase where TTask : Task { private Exception _exception; private TTask _task; protected CatchInfoBase(TTask task) { Contract.Assert(task != null); _task = task; _exception = _task.Exception.GetBaseException(); // Observe the exception early, to prevent tasks tearing down the app domain } /// /// The exception that was thrown to cause the Catch block to execute. /// public Exception Exception { get { return _exception; } } /// /// Returns a CatchResult that re-throws the original exception. /// public CatchResult Throw() { return new CatchResult { Task = _task }; } /// /// Represents a result to be returned from a Catch handler. /// internal struct CatchResult { /// /// Gets or sets the task to be returned to the caller. /// internal TTask Task { get; set; } } } internal class CatchInfo : CatchInfoBase { private static CatchResult _completed = new CatchResult { Task = TaskHelpers.Completed() }; public CatchInfo(Task task) : base(task) { } /// /// Returns a CatchResult that returns a completed (non-faulted) task. /// [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This would result in poor usability.")] public CatchResult Handled() { return _completed; } /// /// Returns a CatchResult that executes the given task and returns it, in whatever state it finishes. /// /// The task to return. [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This would result in poor usability.")] public CatchResult Task(Task task) { return new CatchResult { Task = task }; } /// /// Returns a CatchResult that throws the given exception. /// /// The exception to throw. [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This would result in poor usability.")] public CatchResult Throw(Exception ex) { return new CatchResult { Task = TaskHelpers.FromError(ex) }; } } internal class CatchInfo : CatchInfoBase> { public CatchInfo(Task task) : base(task) { } /// /// Returns a CatchResult that returns a completed (non-faulted) task. /// /// The return value of the task. [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This would result in poor usability.")] public CatchResult Handled(T returnValue) { return new CatchResult { Task = TaskHelpers.FromResult(returnValue) }; } /// /// Returns a CatchResult that executes the given task and returns it, in whatever state it finishes. /// /// The task to return. [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This would result in poor usability.")] public CatchResult Task(Task task) { return new CatchResult { Task = task }; } /// /// Returns a CatchResult that throws the given exception. /// /// The exception to throw. [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This would result in poor usability.")] public CatchResult Throw(Exception ex) { return new CatchResult { Task = TaskHelpers.FromError(ex) }; } } }