27

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?

3 Answers 3

37

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 =

Sign up to request clarification or add additional context in comments.

4 Comments

I'm curious, how does this code honor CTRL+C? The .Net task still executes within the PowerShell process. Simply issuing a CTRL+C to a while loop only breaks out of that PowerShell loop.
@ChrisLynch It doesn't just break out of the loop, it issues a pipeline stop for the current interactive pipeline. In this case that means it also skips the next line. Paste the example above into a prompt, switching the example static method with [Threading.Tasks.Task]::Delay(5000) and you'll see what I mean.
Yes, I have seen that behavior. My question was trying to get at another point: there are two pipelines here, PowerShell and .Net. Doing a CTRL+C only affects PowerShell. To also stop the .Net task/event, you should show Try/Finally block. Finally will be executed when CTRL+C is used, which you could then call a .Stop() or .Halt() method for the .Net async task/event, if supported.
Unfortunately there's no generic way to stop the actual work that a task is performing. Some methods support a cancellation token, some require a different method to be called on another thread, and plenty do not support cancellation at all. FWIW, it's not uncommon for a command to implement StopProcessing by pretending to cancel while still continuing to do work in another thread, even though it will eventually be discarded. While not great, it's still better than ignoring stop processing requests completely (imo).
12

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

1 Comment

The above wasn't working for me, but it seems I was able to pipline using similiar (line breaks matter and this will be missing them): function Await-Task { process { $task = $_ while (-not $task.AsyncWaitHandle.WaitOne(200)) { } $task.GetAwaiter().GetResult() } }
4

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 job 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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.