How to Write a PowerShell Binary Module – revisited
Recently I started writing a PowerShell binary module in C# (mainly due to facilitate testing and move away from Pester limitations). In this post I will describe the format of […]
Audit and Consulting of Information Systems and Business Processes
Recently I started writing a PowerShell binary module in C# (mainly due to facilitate testing and move away from Pester limitations). In this post I will describe the format of […]
Recently I started writing a PowerShell binary module in C# (mainly due to facilitate testing and move away from Pester limitations).
In this post I will describe the format of our PowerShell module and the format of the respective Cmdlets, plus logging and testing integration.
My first attempt was to create a module for Abiquo which is a Cloud Management Platform that allows for end-user self-servicing IaaS resources while abstracting the actual hypervisor and virtualisation technologies.
As we already had written a C# class library I wanted to wrap the library into easier to use Cmdlets. And as mocking a class library (or any native .NET call) in Pester is hard to impossible it seemed to be a good candidate for what I was trying to do.
Our module (named biz.dfch.PS.Abiquo.Client
) will have a module context which will hold several properties such as a reference to the logged in Abiquo server. Similar to VMware PowerCLI that use a $VIServer
variable we use special class called ModuleContext
that will be stored in a variable called $biz_dfch_PS_Abiquo_Client
.
This naming convention we have already been using for our PowerShell script modules (like it or not), so we wanted to keep with it.
As C# assemblies do not support initialisation code (such as DllMain
) we have to perform this in PowerShell. Therefore we define a PowerShell manifest (PSD1) which will list the actual C# assembly (DLL) and a PowerShell module file (PSM1) as nested module.
This has the advantage that the code in PSM1 will be executed at module import (before the first Cmdlet is being called).
Note1: while the PSM1 is called the module is not yet loaded and therefore no Cmdlets inside the module can be called.
# biz.dfch.PS.Abiquo.Client.psd1 @{ # ... # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess NestedModules = @( 'biz.dfch.PS.Abiquo.Client.dll' , 'biz.dfch.PS.Abiquo.Client.psm1' ) # ... }
While we use PowerShell’s native CliXml format for storing configuration settings (with all its drawbacks) we use a ConfigurationSection.
However as PowerShell.exe
is the hosting process of our modules normally this configuration section would have to be defined in PowerShell.exe.config
. This is not feasible as this file might get overwritten by any Windows updates, so therefore we created a separate configuration file name biz.dfch.PS.Abiquo.Client.dll.config
which resides by default in the module directory:
Note: in order to have the ConfigurationManager load this file we have to use OpenMappedMachineConfiguration. All this logic is encapsulated in the Import-Configuration
Cmdlet.
An advantage of using a ConfigurationSection
over plain JSON or XML is its type safety and ability to get encrypted by the .NET runtime.
For example the credential
attribute is automatically converted to a property of type PSCredential
:
After import the module configuration looks like this on the PowerShell console:
Every module uses an extended TraceSource and a custom trace listener from the biz.dfch.CS.Commons package which is also available for configuration via the configuration variable:
The main configuration is done via the PowerShell.exe.config
which will hold an entry like this:
<source name="biz.dfch.PS.Abiquo.Client" switchName="SourceSwitchAll" > <listeners> <clear /> <add name="Log4NetTraceListener" /> </listeners> </source>
All Cmdlets derive from our own PsCmdletBase
so we have a consistent logging upon entry and exit of each Cmdlet (at the Activity level along with their unique event ids):
A note about exception handling: In order to facilitate exception handling and logging we add an event handler to catch all Code Contract Exceptions and send them via the TraceSource
to the standard log.
Furthermore we provided an WriteError
overload in our biz.dfch.CS.PowerShell.Commons package to also write any ErrorRecords
to the common TraceSource.
Note regarding ErrorRecord
s:
we use an ErrorRecord factory to facilitate the generation of error records in a consistent way (all messages and event ids are stored in a resource or defined in an enum respectively):
ErrorRecordFactory.GetNotFound ( Messages.GetMachineIdNotFound, Constants.EventId.GetMachineIdNotFound.ToString(), Id );
As written before the Cmdlets derive from our specialised PsCmdletBase
which derives from PSCmdlet
. In this class we provide some useful overloads, such as
PSDefaultValue
annotationsThe Cmdlets themselves always use a separate method for the parameters set invoked, leaving the ProcessRecord()
method only as a stub with ShouldProcess
support and a witch for dispatch to the parameter set.
All parameter set names are modelled as a nested class inside the Cmdlet:
[Cmdlet( VerbsCommon.Get, "Machine", ConfirmImpact = ConfirmImpact.Low, DefaultParameterSetName = ParameterSets.LIST, SupportsShouldProcess = true, HelpUri = "http://dfch.biz/biz/dfch/PS/Abiquo/Client/Get-Machine/" )] [OutputType(typeof(VirtualMachine))] public class GetMachine : PsCmdletBase { public static class ParameterSets { public const string LIST = "list"; public const string ID = "id"; public const string NAME = "name"; } [Parameter(Mandatory = true, Position = 0, ParameterSetName = ParameterSets.ID)] [ValidateRange(1, int.MaxValue)] public int Id { get; set; } // ... more parameters }
All Cmdlets can access the module configuration variable via ModuleConfiguration.Current
which is a singleton for ModuleContext
.
So logging is then done like this:
Message are all defined in a central resource …
… and can then easily be retrieved like this:
Note: the advantage of using messages with format strings is, that they only have to be actually formatted if the message has an EventType above the level defined at the TraceSource and if the associated listener has a filter that allows this message to be logged.
As already described in Unit Testing C# binary PowerShell Modules we use our biz.dfch.CS.Testing package and invoke a new Pipeline
for every test.
In conjunction with Justmock we can easily mock any object (may it be static or private).
In this case we mock the underlying C# module that makes all the actual REST calls to the Abiquo instance. As this module is already unit and integration tested there is no need perform another set of real tests – a mock is totally sufficient here.
Invoking, mocking and testing a Cmdlet is then a simple matter like in the following example:
[TestMethod] public void InvokeParameterSetIdSucceeds() { Mock.Arrange(() => CurrentClient.IsLoggedIn) .Returns(true); Mock.Arrange(() => CurrentClient.GetAllVirtualMachines()) .Returns(VirtualMachines) .MustBeCalled(); var parameters = @"-Id 42"; var results = PsCmdletAssert.Invoke(sut, parameters); Assert.IsNotNull(results); Assert.AreEqual(1, results.Count); var result = results[0].BaseObject as VirtualMachine; Assert.IsNotNull(result, results[0].BaseObject.GetType().FullName); Assert.AreEqual(42, result.Id); Assert.AreEqual("Edgar", result.Name); Mock.Assert(() => CurrentClient.GetAllVirtualMachines()); }
All in all I pretty much like Cmdlet creation in C#. While some parts are harder to achieve in C# (or more bloated) than in PowerShell I really like the more precise handling in C# (e.g. less unwanted coercion and only explicit returns via WiteOutput
). Plus the advantages in logging, testing and refactoring (and maybe speed of execution) make this approach really shine.
You can find the full source code at GitHub: biz.dfch.PS.Abiquo.Client