1

So I am trying to take an IAsyncEnumerable method and relay the results of said method in PowerShell. I understand that PowerShell does not have async support, and generally, people use the Task.GetAwaiter().GetResult() as the means to get their result.

However, this approach does not work (or at least I don't know how to implement it) for IAsyncEnumerable methods.

My specific case is a little more sophisticated, but let's take this example:

namespace ServerMetadataCache.Client.Powershell
{
    [Cmdlet(VerbsDiagnostic.Test,"SampleCmdlet")]
    [OutputType(typeof(FavoriteStuff))]
    public class TestSampleCmdletCommand : PSCmdlet
    {
        [Parameter(
            Mandatory = true,
            Position = 0,
            ValueFromPipeline = true,
            ValueFromPipelineByPropertyName = true)]
        public int FavoriteNumber { get; set; } = default!;

        [Parameter(
            Position = 1,
            ValueFromPipelineByPropertyName = true)]
        [ValidateSet("Cat", "Dog", "Horse")]
        public string FavoritePet { get; set; } = "Dog";


        private IAsyncEnumerable<int> InternalMethodAsync()
        {
            for (int i = 1; i <= 10; i++)
            {
                await Task.Delay(1000);//Simulate waiting for data to come through. 
                yield return i;
            } 
        }

        protected override void EndProcessing()
        {   
          //this is the issue... how can I tell powershell to read/process results of 
          //InternalMethodAsync()? Regularly trying to read the method doesn't work
          //and neither does wrapping the method with a task and using GetAwaiter().GetResult()
        }

    public class FavoriteStuff
    {
        public int FavoriteNumber { get; set; } = default!;
        public string FavoritePet { get; set; } = default!;
    }

}

This cmdlet is of course a dummy that just takes in a integer and either "Dog", "Cat" or "Horse", but my bigger concern is how to process the InternalMethodAsync() in the Cmdlet. The challenge is getting around the IAsyncEnumerable.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
ThanoojJ
  • 11
  • 3
  • Welcome to StackOverflow. What is the use case that you want to solve with `IAsyncEnumerable`? – Peter Csala Oct 06 '21 at 16:08
  • @PeterCsala Hey. So I was given an API Client in C# that utilizes `IAsyncEnumerable` in processing API requests/responses to AWS API Gateway. I need to create a Binary Cmdlet for other teams that utilize PowerShell to use this Client. I am getting stuck with how to process the `IAsyncEnumerable` methods in PowerShell since PS doesn't offer proper async support (to my knowledge). – ThanoojJ Oct 06 '21 at 16:14
  • I'm not a PowerShell expert. But if PS does not have async support then you have problem even with a single async API Gateway request. Is my understanding correct? – Peter Csala Oct 06 '21 at 16:22
  • @PeterCsala Cmdlets in PowerShell supports streaming output asynchronously via it's own runtime API, but the calling methods will always have to be non-async. I think the "real" question here is "Can we reliably consume an `IAsyncEnumerable` in a non-async context without waiting until it's fully enumerated", eg. is there a pattern for turning an `IAsyncEnumerable` into an `IEnumerable` (I suspect the answer is no, but I'm not particularly comfortable with async in C#) – Mathias R. Jessen Oct 06 '21 at 16:38
  • 1
    Yes, @MathiasR.Jessen, I think that's much better wording that sheds some better light on what I want to do. Async operations are often handled using `Task.GetAwaiter().GetResult()` but I'm not having much luck with that on the `IAsyncEnumerable` here. – ThanoojJ Oct 06 '21 at 16:43

2 Answers2

1

Make an async wrapper method that takes a concurrent collection - like a ConcurrentQueue<int> - as a parameter and fills it with the items from the IAsyncEnumerable<int>, then start reading from it before the task completes:

private async Task InternalMethodExchangeAsync(IProducerConsumerCollection<int> outputCollection)
{
    await foreach(var item in InternalMethodAsync())
        outputCollection.TryAdd(item)
}

Then in EndProcessing():

protected override void EndProcessing()
{
    var resultQueue = new ConcurrentQueue<int>();
    var task = InternalMethodExchangeAsync(resultQueue);

    // Task still running? Let's try reading from the queue!
    while(!task.IsCompleted)
        if(resultQueue.TryDequeue(out int preliminaryResult))
            WriteObject(preliminaryResult);

    // Process remaining items
    while(resultQueue.Count > 0)
        if(resultQueue.TryDequeue(out int trailingResult))
            WriteObject(trailingResult);
}
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • This is a clever solution but it does not seem to print out anything to PowerShell - it builds with no errors and the cmdlet can be run but it does not produce any output. – ThanoojJ Oct 07 '21 at 02:29
0

This question really helped me figure it out.

It Goes back to get Task.GetAwaiter().GetResult() - which works on regular tasks. You need to process the enumerator's results as tasks (which I was trying to do earlier but not in the right way). So based on the InternalMethodAsync() in my question, one could process the results like so:

private void processDummy(){
       var enumerator = InternalMethodAsync().GetAsyncEnumerator();
       while (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult()){
              Console.WriteLine(enumerator.Current);
       }
}

The AsTask() was the key piece here.

ThanoojJ
  • 11
  • 3