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.

Architecture

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.

Module Initialisation

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'
  )

  # ...
}

Configuration Settings

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:

PowerShell Module Folder
PowerShell Module Folder

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:

Module Configuration
Module Configuration

After import the module configuration looks like this on the PowerShell console:

Module Configuration Variable
Module Configuration Variable

Logging

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:

TraceSource
TraceSource

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):

Logging
Logging

Exception Handling

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 ErrorRecords:

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
);
Code Contract Exception Handler Log
Code Contract Exception Handler Log

PSCmdlets

As written before the Cmdlets derive from our specialised PsCmdletBase which derives from PSCmdlet. In this class we provide some useful overloads, such as

  • WriteError
  • BeginProcessing, EndProcessing
  • setting default values from PSDefaultValue annotations

The 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 …

Message Resource
Message Resource

… and can then easily be retrieved like this:

Message Formatting and Logging
Message Formatting and Logging

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.

Testing

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());
}

Conclusion

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

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.