Await async C# method from PowerShell
It'll run fine on its own, but if you want to wait for it to finish you can use this
$null = [MyNamespace.MyClass]::MyStaticMethod($myParam).GetAwaiter().GetResult()
This will unwrap the AggregateException
that would be thrown if you used something like $task.Result
instead.
However that will block until it's complete, which will prevent CTRL + C
from properly stopping the pipeline. You can wait for it to finish while still obeying pipeline stops like this
$task = [MyNamespace.MyClass]::MyStaticMethod($myParam) while (-not $task.AsyncWaitHandle.WaitOne(200)) { } $null = $task.GetAwaiter().GetResult()
If the async method actually returns something, remove $null =
Borrowing from Patrick Meinecke's answer, it's possible to make a pipeline-able function that will resolve a task (or list of tasks) for you:
function Await-Task { param ( [Parameter(ValueFromPipeline=$true, Mandatory=$true)] $task ) process { while (-not $task.AsyncWaitHandle.WaitOne(200)) { } $task.GetAwaiter().GetResult() }}
Usage:
$results = Get-SomeTasks $paramA $paramB | Await-Task
I recently ran into this and found that creating a PowerShell job seems to do the trick pretty nicely as well. This gives you the standard job capabilities (Wait-Job, Receive-Job, and Remove-Job).Jobs can be daunting, but this one's pretty simple. It's written in C# so you may need to add it with Add-Type (will require some tweaks to how it's written, Add-Type -TypeDefintition '...' seems to fail when I use lambdas, so they'd need replaced with proper Get accessors) or compile it.
using System;using System.Management.Automation;using System.Threading;using System.Threading.Tasks;namespace MyNamespace{ public class TaskJob : Job { private readonly Task _task; private readonly CancellationTokenSource? _cts; public override bool HasMoreData => Error.Count > 0 || Output.Count > 0; public sealed override string Location => Environment.MachineName; public override string StatusMessage => _task.Status.ToString(); public override void StopJob() { // to prevent the job from hanging, we'll say the job is stopped // if we can't stop it. Otherwise, we'll cancel _cts and let the // .ContinueWith() invocation set the job's state. if (_cts is null) { SetJobState(JobState.Stopped); } else { _cts.Cancel(); } } protected override void Dispose(bool disposing) { if (disposing) { _task.Dispose(); _cts?.Dispose(); } base.Dispose(disposing); } public TaskJob(string? name, string? command, Task task, CancellationTokenSource? cancellationTokenSource) : base(command, name) { PSJobTypeName = nameof(TaskJob); if (task is null) { throw new ArgumentNullException(nameof(task)); } _task = task; task.ContinueWith(OnTaskCompleted); _cts = cancellationTokenSource; } public virtual void OnTaskCompleted(Task task) { if (task.IsCanceled) { SetJobState(JobState.Stopped); } else if (task.Exception != null) { Error.Add(new ErrorRecord( task.Exception, "TaskException", ErrorCategory.NotSpecified, task) { ErrorDetails = new ErrorDetails($"An exception occurred in the task. {task.Exception}"), } ); SetJobState(JobState.Failed); } else { SetJobState(JobState.Completed); } } } public class TaskJob<T> : TaskJob { public TaskJob(string? name, string? command, Task<T> task, CancellationTokenSource? cancellationTokenSource) : base(name, command, task, cancellationTokenSource) { } public override void OnTaskCompleted(Task task) { if (task is Task<T> taskT) { try { Output.Add(PSObject.AsPSObject(taskT.GetAwaiter().GetResult())); } // error handling dealt with in base.OnTaskCompleted catch { } } base.OnTaskCompleted(task); } }}
After adding this class to your PowerShell session, you can turn a task into a task pretty easily:
$task = [MyNamespace.MyClass]::MyStaticMethod($myParam)$job = ([MyNamespace.TaskJob]::new('MyTaskJob', $MyInvocation.Line, $task, $null))# Add the job to the repository so that it can be retrieved later. This requires that you're using an advanced script or function (has an attribute declaration, particularly [CmldetBinding()] before the param() block). If not, you can always make a Register-Job function to just take an unregistered job and add it to the job repository.$PSCmdlet.JobRepository.Add($job)# now you can do all this with your taskGet-Job 'MyTaskJob' | Wait-JobGet-Job 'MyTaskJob' | Receive-JobGet-Job 'MyTaskJob' | Remove-Job
I will point out I'm not incredibly familiar with tasks, so if anyone sees something that looks bad up there let me know, I'm always looking for ways to improve. :)
A more developed concept can be found in this TaskJob gist.