Await async C# method from PowerShell Await async C# method from PowerShell powershell powershell

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.