Search code examples
powershellpipeline

Deferred pipeline execution in powershell?


Is it possible to delay pipeline execution, or to modify a previous pipeline? What I am looking for is the ability to interact with an ODATA endpoint. I want to use standard (or custom) powershell commands to filter the data, but I don't want to retrieve the whole list. For example

function Get-Records() {
    Invoke-RestMethod -Method Get -Uri $endpoint.Uri.AbsoluteUri ...
}

Calling this could return 500+ records. Normally I would not want to retrieve all 500 records, sometimes I would. So I might just call Get-Records if I need all 500. But if I only wanted specific ones I would want to do

Get-Records | Where {$_.Name -eq 'me'}

The above still receives all 500 records and then filters them down. I would somehow want the Where {$_.Name -eq 'me'} to instead pass back to the previous pipeline a filter to the Invoke-RestMethod and appending to the URI $filter=Name eq 'me'


Solution

  • You cannot modify a pipeline retroactively via a post-processing filter such as Where-Object.

    Instead, you must filter at the source, using the syntax of the data provider.

    This is how PowerShell's built-in cmdlets such as Get-ChildItem do it, via a [string]-typed -Filter parameter.

    If you want to pass a PowerShell script block as the filter, you'll have to translate it to the provider's syntax yourself - if possible.

    There will rarely be a one-to-one mapping of PowerShell expressions to a provider's filter capabilities, so perhaps the better approach is to require the user to use the provider's syntax directly:

    function Get-Records() {
      param(
       [Parameter(Mandatory)]
       [uri] $Uri
       ,
       [string] $Filter # Optional filter in provider syntax; e.g. "Name eq 'me'"
      )
        if ($Filter) { $Uri += '?$filter=' + $Filter }
        Invoke-RestMethod -Method Get -Uri $uri
    }
    
    # Invoke with a filter in the provider's syntax.
    Get-Records "Name eq 'me'"
    

    If you do want the user to be able to pass a script block, you'll have to do your own translation to the provider syntax and ensure that translation is possible.

    To do this robustly, you'd have to process the script block's AST (abstract syntax tree), which is accessible via its .Ast property, which is nontrivial.

    If you're willing to make assumptions about the type of expressions a user is allowed to pass, you can get away with string parsing, such as in the following simplistic example:


    function Get-Records {
      param(
       [Parameter(Mandatory)]
       [uri] $Uri
       ,
       [scriptblock] $FilterScriptBlock # Optional filter
      )
        if ($FilterScriptBlock) { 
          # Translate the script block' *string representation*
          # into the provider-native filter syntax.
          # Note: This is overly simplistic in that it simply removes '$_.'
          #       and '-' before '-eq'.
          $Uri += '?$filter=' + $FilterScriptBlock -replace '\$_\.' -replace '-(?=[a-z]+\b)'
        }
        Invoke-RestMethod -Method Get -Uri $Uri
    }
    
    # Invoke with a filter specified as a PowerShell script block.
    Get-Records { $_.Name -eq 'me' }