PowerShell load .Net Assembly

5 minute read

One common technique is loading .Net assemblies in PowerShell script or module to leverage functionalities otherwise not available natively in PowerShell.

A good example of this is in my PowerScp module which is distributed together with the WinSCPnet.dll assembly which makes required classes and methods available from within the PowerShell session.

What is a .Net Assembly and why use them

Before proceeding let’s formally define of what an assembly is and why we want to use them in our scripts, straight from MSDN an assembly is

…(a) collection of types and resources that are built to work together and form a logical unit of functionality. Assemblies take the form of executable (.exe) or dynamic link library (.dll) files, and are the building blocks of .NET applications. They provide the common language runtime with the information it needs to be aware of type implementations…

If you want to read more about this the full article quoted above is available here.

There are multiple methods we can use to add assemblies to PowerShell which we’re going to explore in the post.

Method 1 - Add assembly with Add-Type cmdlet

Building on the WinSCPnet.dll example let’s how we can import in our PowerShell code make use of the new types and methods made available by it.

Aim of my PowerScp module is creating a wrapper around to the fantastic WinSCP GUI package to be used within PowerShell, to make this possible I leveraged the WinSCP assembly made available via compiled DLL file so to make available functions and objects that are not native to the .Net Framework and PowerShell.

Exploring this by example in the below snippet, taken from the New-ScpSession cmdlet, I’m creating a new object of WinSCP.Session type but without importing the required assemblies PowerShell will throw the following exception:

# This will fail
[WinSCP.Session]$scpSessionObject =  New-Object -TypeName  'WinSCP.Session'

New-Object : Cannot find type [WinSCP.Session]: verify that the assembly containing this type is loaded.
At line:1 char:1
+ New-Object -TypeName  'WinSCP.Session'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidType: (:) [New-Object], PSArgumentException
    + FullyQualifiedErrorId : TypeNotFound,Microsoft.PowerShell.Commands.NewObjectCommand

Error message is rather clear but the important bit is

Cannot find type [WinSCP.Session]: verify that the assembly containing this type is loaded.

As expected PowerShell is complaining as it cannot find a definition for the WinsSCP.Session object this is where adding assemblies to out scripts come into play.

# Define Assembly path
[string]$assemblyPath = 'C:\Assemblies\WinSCPnet.dll'

# Add assembly DLL
Add-Type -Path $assemblyPath

The above will return no output and import into the PowerShell session all classes and methods associated with the DLL.

# Define Assembly path
[string]$assemblyPath = 'C:\Assemblies\WinSCPnet.dll'

# Add assembly DLL
Add-Type -Path $assemblyPath

# Create new object
[WinSCP.Session]$scpSessionObject =  New-Object -TypeName  'WinSCP.Session'

# Get object type and members
$scpSessionObject | Get-Member

TypeName: WinSCP.Session # Note new not native type

Name                          MemberType Definition
----                          ---------- ----------
Failed                        Event      WinSCP.FailedEventHandler Failed(System.Object, WinSCP.FailedEventArgs)
FileTransferProgress          Event      WinSCP.FileTransferProgressEventHandler FileTransferProgress(System.Object,...
FileTransferred               Event      WinSCP.FileTransferredEventHandler FileTransferred(System.Object, WinSCP.Tr...
OutputDataReceived            Event      WinSCP.OutputDataReceivedEventHandler OutputDataReceived(System.Object, Win...
QueryReceived                 Event      WinSCP.QueryReceivedEventHandler QueryReceived(System.Object, WinSCP.QueryR...
Abort                         Method     void Abort()
AddRawConfiguration           Method     void AddRawConfiguration(string setting, string value)
CalculateFileChecksum         Method     byte[] CalculateFileChecksum(string algorithm, string path)
Close                         Method     void Close()
TranslateRemotePathToLocal    Method     string TranslateRemotePathToLocal(string remotePath, string remoteRoot, str...
AdditionalExecutableArguments Property   string AdditionalExecutableArguments {get;set;}
DebugLogLevel                 Property   int DebugLogLevel {get;set;}
DebugLogPath                  Property   string DebugLogPath {get;set;}
DefaultConfiguration          Property   bool DefaultConfiguration {get;set;}
DisableVersionCheck           Property   bool DisableVersionCheck {get;set;}
ExecutablePath                Property   string ExecutablePath {get;set;}
ExecutableProcessPassword     Property   securestring ExecutableProcessPassword {get;set;}
ExecutableProcessUserName     Property   string ExecutableProcessUserName {get;set;}

Note: Above output is snipped for brevity as the assembly supports a lot of methods and properties

Method 2 - Add assembly with Reflection Assembly

This is a legacy method that was replaced by the Add-Type cmdlet in Version 2.0 of PowerShell but still works in current versions syntax is slightly more complex but will get the job done

# Define Assembly path
[string]$assemblyPath = 'C:\Assemblies\WinSCPnet.dll'

# Load assembly and suppress output
[void]([System.Reflection.Assembly]::LoadFrom($assemblyPath))

In the previous example casting void will suppress output which is, by default, printed by the command which would look similar to the this

[System.Reflection.Assembly]::LoadFrom($assemblyPath)

GAC    Version        Location
---    -------        --------
False  v4.0.30319     C:\Assemblies\WinSCPnet.dll

Method 3 - Add assembly via a string object

The methods I have illustrated to this point have similar functionality and will work equally well but both have a drawback they will put a lock on the DLL file they’re using, this means if you need to cleanup the environment, rename, delete or move the file you will not be able to do so as PowerShell will be blocking the file itself. Generally this is not an issue but when you’re developing a solution and need to cleanup the environment rather frequently I can assure it is.

Solution to the above problem is using a string in place of the binary DLL file itself which will give us two distinct advantages fist no lock will be placed on the assembly file, second there is no need for the DLL file to be present on the system where the script or module is running making distribution and portability even easier. Let’s explore how this works.

Step 1 - Get Base64 encoded string

The first step in converting the binary DLL to string is getting Base64 encoded string of it which is achieved with the following command:

# Define Assembly path
[string]$assemblyPath = 'C:\Assemblies\WinSCPnet.dll'

# Read all bytes from DLL
[byte]$assemblyBytes = [System.IO.File]::ReadAllBytes($assemblyPath)

This will read and convert all bytes from the dll specified in the $assemblyPath variable if we pipe variable to the screen output will be similar to this

$assemblyBytes
77
90
144
0
3
0
0
<snip>

Once all the DLL bytes are available we can convert that to a Base64 string using the Convert.ToBase64String Method

[string]$assekblyString = [System.Convert]::ToBase64String($assemblyBytes)

Again piping the variable to the console will produce an output similar to the following

Spoiler Alert: Resulting string will be really long.

$assekblyString
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAABQRQAATAEDAPAmF50AAAAAAAAAAOAAIiALATAAAPoBAAAGAAAAAAAAVhgCAAAgAAAAIAIAAAAAEAAgAAAAAgAABAAAAAAAAAAEAAAAAAAAAABgAgAAAgAAsYsCAAMAQIUAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAAIYAgBPAAAAACACANADAAAAAAAAAAAAAAAAAAAAAAAAAEACAAwAAAAwFwIAVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAACAAAAAAAAAAAAAAACCAAAEgAAAAAAAAAAAAAAC50ZXh0AAAAXPgBAAAgAAAA
<snip>

Step 2 - Embed assembly string in script

Once we have converted the string to distribute required assemblies all we need to do is making the string available in the code but as that will be Base64 encoded we first need to convert it back with the Convert.FromBase64String Method

# Define asembly string
[string]$assekblyString = 'TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAA...'

# Convert string to bytes
[byte]$assemblyBytes = [System.Convert]::FromBase64String($assekblyString)

# Import assembly
[System.Reflection.Assembly]::Load($assemblyBytes)

Note how assembly can only be imported via the System Reflection Assembly method and not the Add-Type one but apart from this functionality in the script will not be impacted and dependency on the binary DLL being available on the system will be removed.

Pro Tip: Keep in mind this method while having advantages comes at the cost of making the script file larger due the overhead of having the full DLL string embedded in the code and will suffer from a slight performance hit due to all the conversion between types. I don’t have enough data to confirm the penalty hit but will carry on some tests.