powershellclassmoduleremote-execution

PowerShell - Running Class code locally vs remote


I am working on a class, which has to run both locally and remote. A simplified version is in the code below.

Assuming that:

The module file C:\Test\MyEmployee.psm1 looks like this:

using namespace System.Collections.Generic

class MyEmployee
{
    [int]$UniqueID
    [string]$Firstname
    [string]$Lastname
    [string]$Folder

    MyEmployee( [int]$ID, [string]$First, [string]$Last, [string]$Folder )
    {
        $this.UniqueID  = $ID
        $this.Firstname = $First
        $this.Lastname  = $Last

        If( $Folder.Length -lt 0 )
        {
            $this.Folder    = $Folder
        }
        Else
        {
            $this.Folder    = "Employee" + $this.UniqueID.ToString( ).PadLeft( 5 , "0" )
        }
    }
}

class MyEmployees
{
    [List[MyEmployee]]$Employees

    MyEmployees( )
    {
        $this.Employees = [List[MyEmployee]]::New( )
    }
}

The calling script looks like this:

using Module "C:\Test\MyEmployee.psm1"

Function RunCode( )
{
    $MyEmps          = [MyEmployees]::New( )
    $MyEmp           = [MyEmployee]::New( 1, "John", "Doe", "" )
    $MyEmps.Employees.Add( $MyEmp )

    ForEach( $MyEmp in $MyEmps.Employees )
    {
        Write-Host "Employee $( $MyEmp.UniqueID ) : $( $MyEmp.Lastname ), $( $MyEmp.Firstname ) "
    }
}

$ScriptBlock = {
    Import-Module "C:\Test\MyEmployee.psm1"

    $MyEmps          = [MyEmployees]::New( )
    $MyEmp           = [MyEmployee]::New( 1, "John", "Doe", "" )
    $MyEmps.Employees.Add( $MyEmp )

    ForEach( $MyEmp in $MyEmps.Employees )
    {
        Write-Host "Employee $( $MyEmp.UniqueID ) : $( $MyEmp.Lastname ), $( $MyEmp.Firstname ) "
    }
}


Clear-Host

$LocalHost  = $Env:ComputerName
$Servername = $LocalHost
#$Servername = "remote"

If( $Servername -eq $LocalHost )
{
    RunCode
}
Else
{
    $LogResult = $( Invoke-Command -ComputerName $ServerName -ScriptBlock $ScriptBlock )
}

When run locally, both on "remote" or on my machine, the code executes perfectly, I get the expected result:

Employee 1 : Doe, John
Employee 2 : Doe, Peter
Employee 3 : Doe, Jane
Employee 4 : Doe, Petra

However, as soon as I un-comment the #$Servername = "remote" line, telling PowerShell to run the ScriptBlock instead of te Function, I get the following error:

Unable to find type [MyEmployees].
    + CategoryInfo          : InvalidOperation: (MyEmployees:TypeName) [], RuntimeException
    + FullyQualifiedErrorId : TypeNotFound
    + PSComputerName        : remote

What am I doing wrong here? Is it even possible to run class code remote like this in PowerShell?


Solution

  • Import-Module doesn't make public the PowerShell classes defined in the psm1, you'll need to use using module in your $scriptblock in the same way as you're doing in your calling script.


    Side Note

    If you're creating a project that has only classes and no functions defined, then using a psm1 is already the incorrect approach, it would be much easier to have a ps1 that you can dot source.

    You could essentially just do:

    $ScriptBlock = {
        . 'C:\Test\MyEmployee.ps1' # <= dot sourcing a ps1 makes all defined classes available in this scope
    
        $MyEmps = [MyEmployees]::New( )
        $MyEmp = [MyEmployee]::New( 1, 'John', 'Doe', '' )
        $MyEmps.Employees.Add( $MyEmp )
    
        ForEach ( $MyEmp in $MyEmps.Employees ) {
            Write-Host "Employee $( $MyEmp.UniqueID ) : $( $MyEmp.Lastname ), $( $MyEmp.Firstname ) "
        }
    }
    

    The next issue if you try to add the using module statement in your $scriptblock will be a parsing error because "A 'using' statement must appear before any other statements in a script." so to workaround that you will need to define your code as a string and then create a scriptblock from it using ScriptBlock.Create.

    In summary:

    $ScriptBlock = [scriptblock]::Create(@'
        using module "C:\Test\MyEmployee.psm1"
    
        $MyEmps = [MyEmployees]::New( )
        $MyEmp = [MyEmployee]::New( 1, 'John', 'Doe', '' )
        $MyEmps.Employees.Add( $MyEmp )
    
        ForEach ( $MyEmp in $MyEmps.Employees ) {
            Write-Host "Employee $( $MyEmp.UniqueID ) : $( $MyEmp.Lastname ), $( $MyEmp.Firstname ) "
        }
    '@)
    

    Then the Invoke-Command statement should work properly assuming C:\Test\MyEmployee.psm1 exists in $ServerName.