Want to skip ahead to the sample module code?

As a developer I’ve long been a fan of Powershell; while there are other shells out there (such as Xonsh) that can do similar things, I tend to keep coming back to PowerShell as an automation language on Windows. Not only does it have a relatively large library of modules available for automating almost every aspect of Windows and related technologies, but the language itself is also surprisingly sophisticated; take parameter sets, for example:

Cmdlets are a lot like functions; they take parameters, and return outputs. What uniquely identifies a Cmdlet is its name (“Verb-Noun”, more or less) and because everything is strongly typed PowerShell can not only tell you if you make a mistake but even suggest correct syntax (i.e. auto-complete). With parameter sets you can declare which combinations of parameters are valid together, while also documenting the scenario targeted by a particular combination of parameters. And standard operators work the way you’d expect:

$items = @('foo', 'bar')
$items += 'baz'

# Items is now ['foo', 'bar', 'baz']

Outside of the Windows world, however, Powershell has obviously had little traction until now.

Core

PowerShell Core (v6.0) is the cross-platform version of Microsoft Powershell; it’s built on .NET Core (and the source can be quite instructive when it comes to the more esoteric aspects of .NET Core infrastructure APIs). If you have existing Powershell scripts or modules, then it’s probably your best bet if you want to target any of the following operating systems:

(I don’t think regular PowerShell is going away, but it’s worth pointing out that like .NET Core, it also works on regular Windows).

Modules

In Powershell, a module groups related functions, Cmdlets, and providers, into a single package that can be utilised via Import-Module.

There are 3 main ways to write PowerShell modules (I’m going to ignore snap-ins because, ugh, why would you ever):

Script modules

Script modules are written almost entirely in the PowerShell scripting language.

Disadvantages of script modules

Advantages of script modules

Binary modules

Binary modules are managed assemblies that use and extend types from System.Management.Automation (although what your project references is typically Microsoft.PowerShell.SDK).

Disadvantages of binary modules

Advantages of binary modules

Manifest modules

Manifest modules are modules whose top-level file is a manifest (.psd1). They can include any number of submodules (whether script, binary, or manifest).

The manifest provides a detailed description of the module and its contents. For example:

#
# Powershell module manifest for CloudControl.
#
# [email protected]

@{
	# Script module or binary module file associated with this manifest.
	RootModule = 'DD.CloudControl.Powershell.dll'

	# Version number of this module.
	ModuleVersion = '1.0'

	# ID used to uniquely identify this module
	GUID = '6b922a5b-b3da-4082-b66c-29f9a4250f81'

	# Author of this module
	Author = '[email protected]'

	# Company or vendor of this module
	CompanyName = 'Dimension Data'

	# Copyright statement for this module
	Copyright = 'Copyright (c) 2017 Dimension Data'

	# Description of the functionality provided by this module
	Description = 'CloudControl'

	# Minimum version of the Windows PowerShell engine required by this module
	PowerShellVersion = '6.0'

	# Type files (.ps1xml) to be loaded when importing this module
	TypesToProcess = @('DD.CloudControl.Powershell.types.ps1xml')

	# Format files (.ps1xml) to be loaded when importing this module
	FormatsToProcess = @('DD.CloudControl.Powershell.format.ps1xml')

	# Export all Cmdlets
	CmdletsToExport = '*'

	# List of all modules packaged with this module
	ModuleList = @('CloudControl')
}

There are many more options you can put in your manifest - have a look at the docs (or run Get-Help New-ModuleManifest -Detailed) if you’re interested.

If you want to include custom type definitions or formatting directives, a manifest-based module is what you want (regardless if whether the underlying module is script-based or binary).

Building a binary module

The tooling for .NET Core (and, consequently, PowerShell Core) module authors hasn’t quite stabilised yet, so these instructions may not be good after RC4. Assuming you are using the RC4 tooling, this is all you have to do in order to get your module working:

  1. mkdir TestModule; cd TestModule
  2. dotnet new lib
  3. dotnet new nugetconfig

Open nuget.config, and make the following changes:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <packageSources>
    <!-- .NET Core nightlies (required by Powershell Core) -->
    <add key="netcore-nightlies" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
    <!-- Powershell Core -->
    <add key="powershell-core" value="https://powershell.myget.org/F/powershell-core/api/v3/index.json" />

    <!-- Attributes used by the help generator -->
    <add key="PSReptile" value="https://www.myget.org/F/ps-reptile/api/v3/index.json"/>
 </packageSources>
</configuration>

Open TestModule.csproj.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard1.4</TargetFramework>
  </PropertyGroup>

</Project>

First off, we need to make a couple of small changes to TestModule.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!--
		PowerShell core requires the NETStandard 1.6.1 package
		(this is still .NET Standard 1.6, but a newer version of its constituent libraries)
	-->
    <NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion>
  </PropertyGroup>

  <PropertyGroup>
  	<!--
	  	PowerShell Core requires .NET Standard 1.6
		(the RC4 tooling generates libraries that target netstandard1.4 by default)
	-->
    <TargetFramework>netstandard1.6</TargetFramework>
  </PropertyGroup>

  <!-- Powershell Core v6.0.0-alpha13 -->
  <ItemGroup>
    <PackageReference Include="Microsoft.PowerShell.SDK" Version="6.0.0-alpha13"/>
    <PackageReference Include="Microsoft.NETCore.Portable.Compatibility" Version="1.0.3-beta-24514-00"/>
  </ItemGroup>

  <!--
    Used for help-related custom attributes; you can leave it out if you don't need it
  -->
  <ItemGroup>
  	<PackageReference Include="PSReptile" Version="0.0.1-alpha1"/>
  </ItemGroup>
</Project>

Run dotnet restore, and we’re ready to add our first Cmdlet:

using PSReptile;
using System.Management.Automation;

namespace SimpleModule
{
    /// <summary>
    ///     A simple Cmdlet that outputs a greeting to the pipeline.
    /// </summary>
	[OutputType(typeof(string))]
    [Cmdlet(VerbsCommon.Get, "Greeting")]
    [CmdletSynopsis("A simple Cmdlet that outputs a greeting to the pipeline")]
    [CmdletDescription(@"
        This Cmdlet works with greetings.
        Give it your name, and it will greet you.
    ")]
    public class GetGreeting
        : Cmdlet
    {
        /// <summary>
        ///     The name of the person to greet.
        /// </summary>
        [ValidateNotNullOrEmpty]
        [Parameter(Mandatory = true, Position = 0, HelpMessage = "The name of the person to greet")]
        public string Name { get; set; }

        /// <summary>
        ///     Perform Cmdlet processing.
        /// </summary>
        protected override void ProcessRecord()
        {
            WriteObject($"Hello, {Name}!");
        }
    }
}

This Cmdlet (Get-Greeting) is simplistic but relatively full-featured. It takes a name (either as -Name xxx or as the first parameter, simply xxx), and writes a greeting to the pipeline.

To actually load your module you’ll need to publish it by running dotnet publish -c release (this will also copy the assemblies your module depends on to the publish directory).

You can then open Powershell and run:

Import-Module './bin/release/netstandard1.6/publish/TestModule.dll'

Get-Greeting -Name 'World'

Cmdlets that call async APIs

Sooner or later, most PowerShell modules wind up calling HttpClient and friends to connect to remote APIs. While you can simply use client.Get("http://foo/bar").Result to synchronously retrieve the response, it’s non-idiomatic (not to mention prone to deadlock). Simply using async / await to call asynchronous APIs is also problematic, however, because Cmdlets really don’t like to be accessed from any thread other than the one that created them.

So a while back, I built a base class called AsyncCmdlet (this being its latest incarnation) that takes care of running asynchronous operations with a special SynchronizationContext that ensures callbacks are always run on the Cmdlet’s owning thread (so you can both use async / await and feel free to call Cmdlet base class methods).

It even offers overloads that support cancellation (via a CancellationToken):

/// <summary>
///     Cmdlet that retrieves information about one or more CloudControl user accounts.
/// </summary>
[OutputType(typeof(UserAccount))]
[Cmdlet(VerbsCommon.Get, Nouns.UserAccount)]
public class GetCloudControlUserAccount
	: CloudControlCmdlet // Inherits from AsyncCmdlet
{
	/// <summary>
	///     Retrieve the current user's account details.
	/// </summary>
	[Parameter(Mandatory = true)]
	public SwitchParameter My { get; set; }

	/// <summary>
	///     Asynchronously perform Cmdlet processing.
	/// </summary>
	/// <param name="cancellationToken">
	///     A <see cref="CancellationToken"/> that can be used to cancel Cmdlet processing.
	/// </param>
	/// <returns>
	///     A <see cref="Task"/> representing the asynchronous operation.
	/// </returns>
	protected override async Task ProcessRecordAsync(CancellationToken cancellationToken)
	{
		CloudControlClient client = GetClient();

		WriteObject(
			await client.GetAccount(cancellationToken) // Cancellation support!
		);
	}
}