I do not mean to sound too cute with the question, but that really is the question at hand. Consider the following two functions defined in a PowerShell module Test.psm1 installed under $env:PSModulePath:
function Start-TestAsync
{
[CmdletBinding()]
param([ScriptBlock]$Block, [string]$Name = '')
Start-Job { Start-Test -Name $using:Name -Block $using:Block }
}
function Start-Test
{
[CmdletBinding()]
param([ScriptBlock]$Block, [string]$Name = '')
# do some work here, including this:
Invoke-Command -ScriptBlock $Block
}
After importing the module, I can then run the synchronous function...
PS> Start-Test -Name "My Test" -Block { ps | select -first 9 }
...and it displays appropriate output from Get-Process.
However, when I attempt to run the asynchronous version...
PS> $testJob=Start-TestAsync -Name "My Test" -Block { ps | select -first 9 }
...and then review its output...
PS> Receive-Job $testJob
... it fails at just bringing in the parameter to the Start-Test function, reporting it cannot convert a String to a ScriptBlock. Thus, -Block $using:Block
is passing a String rather than a ScriptBlock!
After some experimentation, I did find a workaround. If I modify Start-Test so that the type of the $Block parameter is [string] instead of [ScriptBlock] -- and then convert that string back to a block to feed to Invoke-Command...
function Start-Test
{
[CmdletBinding()]
param([string]$Block, [string]$Name = '')
$myBlock = [ScriptBlock]::Create($Block)
Invoke-Command -ScriptBlock $myBlock
}
I then obtain the correct result when I run the same commands from above:
PS> $testJob=Start-TestAsync -Name "My Test" -Block { ps | select -first 9 }
PS> Receive-Job $testJob
Is the using
scope working correctly in my initial example (converting a ScriptBlock to a string)? The limited documentation on it (about_Remote_Variables, about_Scopes) offers little guidance.
Ultimately, is there a way to make Start-Test work when its $Block parameter is typed as a [ScriptBlock]?
While it is useful to know (thanks, @KeithHill) that what I was seeing was a known issue--sorry, I meant "by design"--my real question had not been answered ("Ultimately, is there a way to make Start-Test work when its $Block parameter is typed as a [ScriptBlock]?")
The answer came to me suddenly last night:
function Start-TestAsync
{
[CmdletBinding()]
param([ScriptBlock]$Block, [string]$Name = '')
Start-Job {
$myBlock = [ScriptBlock]::Create($using:Block);
Start-Test -Name $using:Name -Block $myBlock }
}
function Start-Test
{
[CmdletBinding()]
param([ScriptBlock]$Block, [string]$Name = '')
# do some work here, including this:
Invoke-Command -ScriptBlock $Block
}
Notice that in Start-TestAsync I internally allow the serialization to occur ($using:Block), converting the ScriptBlock to a String, then immediately re-convert it (Create) to a ScriptBlock, and can then safely pass that on to Start-Test as a genuine ScriptBlock. To me, this is a significant improvement over the workaround in my question because now the public APIs on both functions are correct.