I want to call a static async C# method from PowerShell by using the static member accessor, such as:
PowerShell
function CallMyStaticMethod([parameter(Mandatory=$true)][string]$myParam)
{
...
[MyNamespace.MyClass]::MyStaticMethod($myParam)
...
}
C#
public static async Task MyStaticMethod(string myParam)
{
...
await ...
...
}
Will my C# method run properly without some sort of "await" call from PowerShell since my C# method is async?
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 task
Get-Job 'MyTaskJob' | Wait-Job
Get-Job 'MyTaskJob' | Receive-Job
Get-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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With