How to create a Standard Library binary module
I recently had an idea for module that I wanted to implement as a binary module. I have yet to create one using the PowerShell Standard Library so this felt like a good opportunity. I used the Creating a cross-platform binary module guide to create this module without any roadblocks. We're going to walk that same process and I'll add a little extra commentary along the way.
Note
The original version of this article appeared on the blog written by @KevinMarquette. The PowerShell team thanks Kevin for sharing this content with us. Please check out his blog at PowerShellExplained.com.
What's the PowerShell Standard Library?
The PowerShell Standard Library allows us to create cross platform modules that work in both PowerShell and Windows PowerShell 5.1.
Why binary modules?
When you are writing a module in C# you give up easy access to PowerShell cmdlets and functions. But if you are creating a module that doesn't depend on a lot of other PowerShell commands, the performance benefit can be significant. PowerShell was optimized for the administrator, not the computer. By switching to C#, you get to shed the overhead added by PowerShell.
For example, we have a critical process that does a lot of work with JSON and hashtables. We optimized the PowerShell as much as we could but the process still takes 12 minutes to complete. The module already contained a lot of C# style PowerShell. This makes conversion to a binary module clean and simple. By converting to a binary module, we reduced the process time from over 12 minutes to under four minutes.
Hybrid modules
You can mix binary cmdlets with PowerShell advanced functions. Everything you know about script
modules applies the same way. The empty psm1
file is included so you can add other PowerShell
functions later.
Almost all of the compiled cmdlets that I have created started out as PowerShell functions first. All of our binary modules are really hybrid modules.
Build scripts
I kept the build script simple here. I generally use a large Invoke-Build
script as part of my
CI/CD pipeline. It does more magic like running Pester tests, running PSScriptAnalyzer, managing
versioning, and publishing to the PSGallery. Once I started using a build script for my modules, I
was able to find lots of things to add to it.
Planning the module
The plan for this module is to create a src
folder for the C# code and structure the rest like I
would for a script module. This includes using a build script to compile everything into an Output
folder. The folder structure looks like this:
MyModule
├───src
├───Output
│ └───MyModule
├───MyModule
│ ├───Data
│ ├───Private
│ └───Public
└───Tests
Getting Started
First I need to create the folder and create the git repo. I'm using $module
as a placeholder for
the module name. This should make it easier for you to reuse these examples if needed.
$module = 'MyModule'
New-Item -Path $module -Type Directory
Set-Location $module
git init
Then create the root level folders.
New-Item -Path 'src' -Type Directory
New-Item -Path 'Output' -Type Directory
New-Item -Path 'Tests' -Type Directory
New-Item -Path $module -Type Directory
Binary module setup
This article is focused on the binary module so that's where we'll start. This section pulls examples from the Creating a cross-platform binary module guide. Review that guide if you need more details or have any issues.
First thing we want to do is check the version of the dotnet core SDK that we have installed. I'm using 2.1.4, but you should have 2.0.0 or newer before continuing.
PS> dotnet --version
2.1.4
I'm working out of the src
folder for this section.
Set-Location 'src'
Using the dotnet command, create a new class library.
dotnet new classlib --name $module
This created the library project in a subfolder but I don't want that extra level of nesting. I'm going to move those files up a level.
Move-Item -Path .\$module\* -Destination .\
Remove-Item $module -Recurse
Set the .NET core SDK version for the project. I have the 2.1 SDK so I'm going to specify 2.1.0
.
Use 2.0.0
if you're using the 2.0 SDK.
dotnet new globaljson --sdk-version 2.1.0
Add the PowerShell Standard Library NuGet package to the project. Make sure you use the most recent version available for the level of compatibility that you need. I would default to the latest version but I don't think this module leverages any features newer than PowerShell 3.0.
dotnet add package PowerShellStandard.Library --version 7.0.0-preview.1
We should have a src folder that looks like this:
PS> Get-ChildItem
Directory: \MyModule\src
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 7/14/2018 9:51 PM obj
-a---- 7/14/2018 9:51 PM 86 Class1.cs
-a---- 7/14/2018 10:03 PM 259 MyModule.csproj
-a---- 7/14/2018 10:05 PM 45 global.json
Now we're ready to add our own code to the project.
Building a binary cmdlet
We need to update the src\Class1.cs
to contain this starter cmdlet:
using System;
using System.Management.Automation;
namespace MyModule
{
[Cmdlet( VerbsDiagnostic.Resolve , "MyCmdlet")]
public class ResolveMyCmdletCommand : PSCmdlet
{
[Parameter(Position=0)]
public Object InputObject { get; set; }
protected override void EndProcessing()
{
this.WriteObject(this.InputObject);
base.EndProcessing();
}
}
}
Rename the file to match the class name.
Rename-Item .\Class1.cs .\ResolveMyCmdletCommand.cs
Then we can build our module.
PS> dotnet build
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restore completed in 18.19 ms for C:\workspace\MyModule\src\MyModule.csproj.
MyModule -> C:\workspace\MyModule\src\bin\Debug\netstandard2.0\MyModule.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.19
We can call Import-Module
on the new dll to load our new cmdlet.
PS> Import-Module .\bin\Debug\netstandard2.0\$module.dll
PS> Get-Command -Module $module
CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Resolve-MyCmdlet 1.0.0.0 MyModule
If the import fails on your system, try updating .NET to 4.7.1 or newer. The Creating a cross-platform binary module guide goes into more details on .NET support and compatibility for older versions of .NET.
Module manifest
It's cool that we can import the dll and have a working module. I like to keep going with it and create a module manifest. We need the manifest if we want to publish to the PSGallery later.
From the root of our project, we can run this command to create the module manifest that we need.
$manifestSplat = @{
Path = ".\$module\$module.psd1"
Author = 'Kevin Marquette'
NestedModules = @('bin\MyModule.dll')
RootModule = "$module.psm1"
FunctionsToExport = @('Resolve-MyCmdlet')
}
New-ModuleManifest @manifestSplat
I'm also going to create an empty root module for future PowerShell functions.
Set-Content -Value '' -Path ".\$module\$module.psm1"
This allows me to mix both normal PowerShell functions and binary cmdlets in the same project.
Building the full module
I compile everything together into an output folder. We need to create a build script to do that. I
would normally add this to an Invoke-Build
script, but we can keep it simple for this example. Add
this to a build.ps1
at the root of the project.
$module = 'MyModule'
Push-Location $PSScriptRoot
dotnet build $PSScriptRoot\src -o $PSScriptRoot\output\$module\bin
Copy-Item "$PSScriptRoot\$module\*" "$PSScriptRoot\output\$module" -Recurse -Force
Import-Module "$PSScriptRoot\Output\$module\$module.psd1"
Invoke-Pester "$PSScriptRoot\Tests"
These commands build our DLL and place it into our output\$module\bin
folder. It then copies the
other module files into place.
Output
└───MyModule
├───MyModule.psd1
├───MyModule.psm1
└───bin
├───MyModule.deps.json
├───MyModule.dll
└───MyModule.pdb
At this point, we can import our module with the psd1 file.
Import-Module ".\Output\$module\$module.psd1"
From here, we can drop the .\Output\$module
folder into our $env:PSModulePath
directory and it
autoloads our command whenever we need it.
Update: dotnet new PSModule
I learned that the dotnet
tool has a PSModule
template.
All the steps that I outlined above are still valid, but this template cuts many of them out. It's still a fairly new template that's still getting some polish placed on it. Expect it to keep getting better from here.
This is how you use install and use the PSModule template.
dotnet new -i Microsoft.PowerShell.Standard.Module.Template
dotnet new psmodule
dotnet build
Import-Module "bin\Debug\netstandard2.0\$module.dll"
Get-Module $module
This minimally-viable template takes care of adding the .NET SDK, PowerShell Standard Library, and creates an example class in the project. You can build it and run it right away.
Important details
Before we end this article, here are a few other details worth mentioning.
Unloading DLLs
Once a binary module is loaded, you can't really unload it. The DLL file is locked until you unload it. This can be annoying when developing because every time you make a change and want to build it, the file is often locked. The only reliable way to resolve this is to close the PowerShell session that loaded the DLL.
VS Code reload window action
I do most of my PowerShell dev work in VS Code. When I'm working on a binary module (or a
module with classes), I've gotten into the habit of reloading VS Code every time I build.
Ctrl+Shift+P pops the command window and Reload Window
is always
at the top of my list.
Nested PowerShell sessions
One other option is to have good Pester test coverage. Then you can adjust the build.ps1 script to start a new PowerShell session, perform the build, run the tests, and close the session.
Updating installed modules
This locking can be annoying when trying to update your locally installed module. If any session has it loaded, you have to go hunt it down and close it. This is less of an issue when installing from a PSGallery because module versioning places the new one in a different folder.
You can set up a local PSGallery and publish to that as part of your build. Then do your local install from that PSGallery. This sounds like a lot of work, but this can be as simple as starting a docker container. I cover a way to do that in my post on Using a NuGet server for a PSRepository.
Final thoughts
I didn't touch on the C# syntax for creating a cmdlet, but there is plenty of documentation on it in the Windows PowerShell SDK. It's definitely something worth experimenting with as a stepping stone into more serious C#.