PowerShell: Unable to find type when using PS 5 classes
Asked Answered
C

5

19

I'm using classes in PS with WinSCP PowerShell Assembly. In one of the methods I'm using various types from WinSCP.

This works fine as long as I already have the assembly added - however, because of the way PowerShell reads the script when using classes (I assume?), an error is thrown before the assembly could be loaded.

In fact, even if I put a Write-Host at the top, it will not load.

Is there any way of forcing something to run before the rest of the file is parsed?

Transfer() {
    $this.Logger = [Logger]::new()
    try {

        Add-Type -Path $this.Paths.WinSCP            
        $ConnectionType = $this.FtpSettings.Protocol.ToString()
        $SessionOptions = New-Object WinSCP.SessionOptions -Property @{
            Protocol = [WinSCP.Protocol]::$ConnectionType
            HostName = $this.FtpSettings.Server
            UserName = $this.FtpSettings.Username
            Password = $this.FtpSettings.Password
        }

Results in an error like this:

Protocol = [WinSCP.Protocol]::$ConnectionType
Unable to find type [WinSCP.Protocol].

But it doesn't matter where I load the assembly. Even if I put the Add-Type cmdlet on the topmost line with a direct path to WinSCPnet.dll, it won't load - it detects the missing types before running anything, it seems.

Cockneyfy answered 16/3, 2017 at 14:49 Comment(0)
B
28

As you've discovered, PowerShell refuses to run scripts that contain class definitions that reference then-unavailable (not-yet-loaded) types - the script-parsing stage fails.

  • As of PowerShell (Core) 7 version 7.4.3 (current as of this writing), even a using assembly statement at the top of a script does not help in this case, because in your case the type is referenced in the context of a PS class definition - this may eventually get fixed, however; a fix has been green-lit, but no one has stepped up to implement it; the required work, along with other class-related issues, is being tracked in GitHub issue #6652.

The proper solution is to create a script module (*.psm1) whose associated manifest (*.psd1) declares the assembly containing the referenced types a prerequisite, via the RequiredAssemblies key.

See alternative solution at the bottom if using modules is not an option.

Here's a simplified walk-through:

Create test module tm as follows:

  • Create module folder ./tm and manifest (*.psd1) in it:

      # Create module folder (remove a preexisting ./tm folder if this fails).
      $null = New-Item -Type Directory -ErrorAction Stop ./tm
    
      # Create manifest file that declares the WinSCP assembly a prerequisite.
      # Modify the path to the assembly as needed; you may specify a relative path, but
      # note that the path must not contain variable references (e.g., $HOME).
      New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 `
        -RequiredAssemblies C:\path\to\WinSCPnet.dll
    
  • Create the script module file (*.psm1) in the module folder:

Create file ./tm/tm.psm1 with your class definition; e.g.:

    class Foo {
      # As a simple example, return the full name of the WinSCP type.
      [string] Bar() {
        return [WinSCP.Protocol].FullName
      }
    }

Note: In the real world, modules are usually placed in one of the standard locations defined in $env:PSMODULEPATH, so that the module can be referenced by name only, without needing to specify a (relative) path.

Use the module:

PS> using module ./tm; [Foo]::new().Bar()
WinSCP.Protocol

The using module statement imports the module and - unlike Import-Module - also makes the class defined in the module available to the current session.

Since importing the module implicitly loaded the WinSCP assembly thanks to the RequiredAssemblies key in the module manifest, instantiating class Foo, which references the assembly's types, succeeded.


If you need to determine the path to the dependent assembly dynamically in order to load it or even to ad-hoc-compile one (in which case use of a RequiredAssemblies manifest entry isn't an option), you should be able to use the approach recommended in Justin Grote's helpful answer - i.e., to use a ScriptsToProcess manifest entry that points to a *.ps1 script that calls Add-Type to dynamically load dependent assemblies before the script module (*.psm1) is loaded - but this doesn't actually work as of PowerShell 7.3.4 (the version current as of this writing): while the definition of the class in the *.psm1 file relying on the dependent assembly's types succeeds, the caller doesn't see the class until a script with a using module ./tm statement is executed a second time:

  • Create a sample module:
# Create module folder (remove a preexisting ./tm folder if this fails).
$null = New-Item -Type Directory -ErrorAction Stop ./tm

# Create a helper script that loads the dependent
# assembly.
# In this simple example, the assembly is created dynamically,
# with a type [demo.FooHelper]
@'
Add-Type @"
namespace demo {
  public class FooHelper {
  }
}
"@
'@ > ./tm/loadAssemblies.ps1

# Create the root script module.
# Note how the [Foo] class definition references the
# [demo.FooHelper] type created in the loadAssemblies.ps1 script.
@'
class Foo {
  # Simply return the full name of the dependent type.
  [string] Bar() {
    return [demo.FooHelper].FullName
  }
}
'@ > ./tm/tm.psm1

# Create the manifest file, designating loadAssemblies.ps1
# as the script to run (in the caller's scope) before the
# root module is parsed.
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 -ScriptsToProcess loadAssemblies.ps1
  • Now, still as of PowerShell 7.3.4, trying to use the module's [Foo] class inexplicably succeeds only after calling using module ./tm twice - which you cannot do in a single script, rendering this approach useless for now:
# Up to at least PowerShell 7.3.4
# !! First attempt FAILS:
PS> using module ./tm; [Foo]::new().Bar()
InvalidOperation: Unable to find type [Foo]

# Second attempt: OK
PS> using module ./tm; [Foo]::new().Bar()
demo.FooHelper

The problem is a known one, as it turns out, and dates back to 2017 - see GitHub issue #2962


If your use case doesn't allow the use of modules:

  • In a pinch, you can use Invoke-Expression, but note that it's generally better to avoid Invoke-Expression in the interest of robustness and so as to avoid security risks[1] .
# Adjust this path as needed.
Add-Type -LiteralPath C:\path\to\WinSCPnet.dll

# By placing the class definition in a string that is invoked at *runtime*
# via Invoke-Expression, *after* the WinSCP assembly has been loaded, the
# class definition succeeds.
Invoke-Expression @'
class Foo {
  # Simply return the full name of the WinSCP type.
  [string] Bar() {
    return [WinSCP.Protocol].FullName
  }
}
'@

[Foo]::new().Bar()
  • Alternatively, use a two-script approach:
    • A main script that loads the dependent assemblies,
    • which then dot-sources a second script that contains the class definitions relying on the types from the dependent assemblies.

This approach is demonstrated in Takophiliac's helpful answer.


[1] It's not a concern in this case, but generally, given that Invoke-Expression can invoke any command stored in a string, applying it to strings not fully under your control can result in the execution of malicious commands - see this answer for more information. This caveat applies to other language analogously, such as to Bash's built-in eval command.

Bagworm answered 16/3, 2017 at 16:40 Comment(2)
I see that this has been updated recently. Is this still up-to-date for 7.3.4? Is it possible to create a type without a .dll assembly? Must C# be used, or can everything be written in PowerShell? If so, where can I find an example?Voroshilovsk
@lit, yes, I think the answer is still up to date (I just updated the version references). Using PowerShell class definitions creates .NET types too, but there are still cases where you need to resort to C#.Bagworm
S
6

An additional solution is to put your Add-Type logic into a separate .ps1 file (name it AssemblyBootStrap.ps1 or something) and then add it to the ScriptsToProcess section of your module manifest. ScriptsToProcess runs before the root script module (*.psm1), and the assemblies will be loaded at the time the class definitions are looking for them.

Selfgovernment answered 8/6, 2021 at 4:49 Comment(2)
That is promising, and the sequence of processing is as you describe, but while defining a class relying on Add-Type-loaded assemblies in the ScriptsToProcess script then succeeds, such a class doesn't surface in the caller's scope in a script that (of necessity) starts with using module - until the second time that script is run (as of PowerShell Core 7.2.0-preview.9).Bagworm
The problem is a known one, as it turns out, and was reported back in 2017 - see GitHub issue #2962. I've added a self-contained reproducible case to my answer.Bagworm
F
5

First, I would recommend mklement0's answer.

However, there is a bit of running around you can do to get much the same effect with a bit less work, which can be helpful in smaller projects or in the early stages.

It's possible to merely . source another ps1 file in your code which contains your classes referencing a not yet loaded library after you load the referenced assembly.

##########
MyClasses.ps1

Class myClass
{
     [3rdParty.Fancy.Object] $MyFancyObject
}

Then you can call your custom class library from your main script with a .

#######
MyMainScriptFile.ps1

#Load fancy object's library
Import-Module Fancy.Module #If it's in a module
Add-Type -Path "c:\Path\To\FancyLibrary.dll" #if it's in a dll you have to reference

. C:\Path\to\MyClasses.ps1

The original parsing will pass muster, the script will start, your reference will be added, and then as the script continues, the . sourced file will be read and parsed, adding your custom classes without issue as their reference library is in memory by the time the code is parsed.

It's still very much better to make and use a module with the proper manifest, but this will get by easy and is very easy to remember and use.

Fredericksburg answered 12/11, 2019 at 16:59 Comment(0)
C
2

Although it's not the solution per se, I worked around it. However, I'll leave the question open as it still stands

Instead of using WinSCP-types, I just use strings. Seeing as I already have enumerals that are identical to WinSCP.Protocol

Enum Protocols {
    Sftp
    Ftp
    Ftps
}

And have set Protocol in FtpSettings

$FtpSettings.Protocol = [Protocols]::Sftp

I can set the protocol like this

$SessionOptions = New-Object WinSCP.SessionOptions -Property @{
            Protocol = $this.FtpSettings.Protocol.ToString()
            HostName = $this.FtpSettings.Server
            UserName = $this.FtpSettings.Username
            Password = $this.FtpSettings.Password
        }

I used similar on [WinSCP.TransferMode]

$TransferOptions.TransferMode = "Binary" #[WinSCP.TransferMode]::Binary
Cockneyfy answered 16/3, 2017 at 15:22 Comment(0)
G
0

Add-Type -LiteralPath C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.IO.Compression.FileSystem.dll $psclasses = GC "C:\Windows\Temp\foobarclass.ps1" -Raw Invoke-Expression $psclasses [Foo]::new().Bar()

Guffaw answered 3/2, 2023 at 19:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.