PowerShell: use "using"-like functionality in jobs to import modules including classes

Viewed 47

I have a solution in Powershell (7) where I have a module that includes a class, like

test.psm1

class TestClass {
    [string]$Test

    TestClass(
        [string]$_test
    ) {

        $this.Test = $_test
    }

    [void] TestMethod() {
        xxx
    }
}

In the main script I can successfully import the module including its classes and work with it.

In the main script I also want to process certain steps in parallel using jobs, like

using module .\test.psm1

[TestClass]$test = [TestClass]::new()
$job = $objects | ForEach-Object -Parallel {
    [TestClass]$testJob = $using:test   
} -ThrottleLimit $ParallelJobsThrottleLimit -AsJob

In these jobs I want to use an object of type TestClass that has been declared outside of the parallel statement.

All my approaches so far failed with Unable to find type.

How can I make this work?

1 Answers

As of PowerShell 7.2.x, the instances of a script block ({ ... }) passed to ForEach-Object -Parallel, which run in parallel runspaces, do not see any of the caller's runspace state, which includes class definitions.

  • Hence the need for the $using: scope to reference values stored in the caller's variables.

  • Unfortunately, there is no equivalent mechanism for custom classes.

  • GitHub issue #12240 discusses a future enhancement that will allow parallel runspaces to see at least parts of the caller's state, which would hopefully included classes.

The problem, in a nutshell:

  • In order for parallel runspaces to see your [TestClass] too, they would have to import its containing module.

  • However, only when you import a module via using module are a module's classes visible to the caller (not also when you use Import-Module).

  • Unfortunately, using statements may only be placed at the start of a script file, and therefore cannot be used inside script blocks.

There are workarounds:


Modify your module to "export" its classes in a way that doesn't require using module:

  • Define your class in a regular .ps1 script that you reference from the ScriptsToProcess entry of your module's manifest file (.psd1)

  • That way, when the module is first imported into a runspace, the .ps1 script is dot-sourced in the caller's (importer's) scope, and class definitions therefore become visible.

  • This allows you to use a regular Import-Module call in your parallel script block in order to see the class definition there.

Note that your module itself will not see such class definitions by default - unless you explicitly dot-source the .ps1 file from the module's top-level code too.


Use a helper script containing a using module statement that you dot-source from your parallel runspaces.

You discovered this workaround yourself:

  • Create a .ps1 script that contains a using module .\test.psm1 call, e.g., Helper.ps1

  • From your parallel script block, dot-source this helper script (. .\Helper.ps1), which will make the module's classes available there.

Related