Creating a PowerShell Module in C# .NET Core

Thu Aug 12 2021Programming

Let's create a PowerShell Module in C# .NET Core with unit testing.

Note: This demonstration uses the dotnet core CLI to initialise and set up the project before heading into Visual Studio. This is my personal preference but it might be advantageous to see the CLI in action.

Follow along with the GitHub repository for this article here.

Getting Started

You may need to install the PowerShell template for .NET Core if you haven't already:

dotnet new -i Microsoft.PowerShell.Standard.Module.Template

Confirm the presence of the template:

dotnet new --list

You should see one line within the output stating the following:

PowerShell Standard Module                    psmodule             [C#]        Library/PowerShell/Module

Creating the Test Project

Using the command line in a directory of your choosing, enter the following commands one-by-one:

# Create the new solution file (PowerShellModuleTest.sln)
dotnet new sln -o PowerShellModuleTest

# Change into the project directory
cd PowerShellModuleTest

# Create a new project based on the psmodule template (PowerShellModuleTest.Cmdlet) 
dotnet new psmodule -o PowerShellModuleTest.Cmdlet

# Add the commandlet project to the solution
dotnet sln add PowerShellModuleTest.Cmdlet

The default module from the template takes in two parameters and essentially wraps them in an object and echoes them back out:

using System.Management.Automation;

namespace PowerShellModuleTest.Cmdlet
{
    [Cmdlet(VerbsDiagnostic.Test, "SampleCmdlet")]
    [OutputType(typeof(FavoriteStuff))]
    public class TestSampleCmdletCommand : PSCmdlet
    {
        [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
        public int FavoriteNumber { get; set; }

        [Parameter(Position = 1, ValueFromPipelineByPropertyName = true)]
        [ValidateSet("Cat", "Dog", "Horse")]
        public string FavoritePet { get; set; } = "Dog";

        // This method gets called once for each cmdlet in the pipeline when the pipeline starts executing
        protected override void BeginProcessing()
        {
            WriteVerbose("Begin!");
        }

        // This method will be called for each input received from the pipeline to this cmdlet; if no input is received, this method is not called
        protected override void ProcessRecord()
        {
            WriteObject(new FavoriteStuff
            {
                FavoriteNumber = FavoriteNumber,
                FavoritePet = FavoritePet
            });
        }

        // This method will be called once at the end of pipeline execution; if no input is received, this method is not called
        protected override void EndProcessing()
        {
            WriteVerbose("End!");
        }
    }

    public class FavoriteStuff
    {
        public int FavoriteNumber { get; set; }
        public string FavoritePet { get; set; }
    }
}

Running the Commandlet:

First we need to import our .DLL with the commandlet in. From the root directory (with the .sln file) run the following commands:

# Build the project in release mode at the root of the directory in a build folder
dotnet build --configuration Release --output ./build .\PowerShellModuleTest.Cmdlet\

# Import the DLL that contains our commandlet
Import-Module ./build/PowerShellModuleTest.Cmdlet.dll

With our module loaded, run the commandlet:

Test-SampleCmdlet -FavoriteNumber 51 -FavoritePet Cat

This will produce the following output (wrapped in an object and echoed back):

FavoriteNumber FavoritePet
-------------- -----------
            51 Cat

Adding Unit Tests

From within the same directory as the .sln file, run the following:

# Create the unit test project
dotnet new mstest -o .\PowerShellModuleTest.UnitTests

# Add the unit test project to the solution
dotnet sln add .\PowerShellModuleTest.UnitTests

# Adds the PowerShellModuleTest.Cmdlet as a reference to the PowerShellModuleTest.UnitTests project
dotnet add .\PowerShellModuleTest.UnitTests\ reference .\PowerShellModuleTest.Cmdlet\

# Add the Microsoft.PowerShell.SDK to PowerShellModuleTest.UnitTests to allow Invoking our commandlet
dotnet add .\PowerShellModuleTest.UnitTests\ package Microsoft.PowerShell.SDK

Replace the contents of UnitTest1.cs with the following:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerShellModuleTest.Cmdlet;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace PowerShellModuleTest.UnitTests
{
    [TestClass]
    public class UnitTest1
    {
        private Runspace _runspace;

        [TestInitialize]
        public void Init()
        {
            var initialSessionState = InitialSessionState.CreateDefault();
            initialSessionState.Commands.Add(
                new SessionStateCmdletEntry("Test-SampleCmdlet", typeof(TestSampleCmdletCommand), null)
            );

            _runspace = RunspaceFactory.CreateRunspace(initialSessionState);
            _runspace.Open();
        }

        [TestMethod]
        public void Basic()
        {
            using var powershell = PowerShell.Create(_runspace);

            // Configure Command
            var command = new Command("Test-SampleCmdlet");
            command.Parameters.Add("FavoriteNumber", 51);
            command.Parameters.Add("FavoritePet", "Cat");
            powershell.Commands.AddCommand(command);
            
            // Run Command
            var result = powershell.Invoke<FavoriteStuff>()[0];

            // Assert
            Assert.IsInstanceOfType(result, typeof(FavoriteStuff));
            Assert.AreEqual(result.FavoriteNumber, 51);
            Assert.AreEqual(result.FavoritePet, "Cat");
        }
    }
}

Running the Unit test creates a PowerShell instance loaded with parameters and tests the response.

Second Example - TestMultiplyCmdletCommand

Let's write another commandlet to multiply two values:

The commandlet:

using System.Management.Automation;

namespace PowerShellModuleTest.Cmdlet
{
    [Cmdlet(VerbsDiagnostic.Test, "MultiplyCmdlet")]
    [OutputType(typeof(TestMultiplyCmdletCommandResponse))]
    public class TestMultiplyCmdletCommand : PSCmdlet
    {
        [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
        public int FirstValue { get; set; }

        [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
        public int SecondValue { get; set; }

        protected override void ProcessRecord()
        {
            WriteObject(new TestMultiplyCmdletCommandResponse
            {
                Result = FirstValue * SecondValue
            });
        }
    }

    public class TestMultiplyCmdletCommandResponse
    {
        public int Result { get; set; }
    }
}

The Unit Test:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerShellModuleTest.Cmdlet;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace PowerShellModuleTest.UnitTests
{
    [TestClass]
    public class TestMultiplyCmdletCommandTest
    {
        private Runspace _runspace;

        [TestInitialize]
        public void Init()
        {
            var initialSessionState = InitialSessionState.CreateDefault();
            initialSessionState.Commands.Add(
              new SessionStateCmdletEntry("Test-MultiplyCmdlet", typeof(TestMultiplyCmdletCommand), null)
            );

            _runspace = RunspaceFactory.CreateRunspace(initialSessionState);
            _runspace.Open();
        }

        [TestMethod]
        public void Run()
        {
            using var powershell = PowerShell.Create(_runspace);

            // Configure Command
            var command = new Command("Test-MultiplyCmdlet");
            command.Parameters.Add("FirstValue", 4);
            command.Parameters.Add("SecondValue", 8);
            powershell.Commands.AddCommand(command);
            
            // Run Command
            var result = powershell.Invoke<TestMultiplyCmdletCommandResponse>()[0];

            // Assert
            Assert.IsInstanceOfType(result, typeof(TestMultiplyCmdletCommandResponse));
            Assert.AreEqual(result.Result, 32);
        }
    }
}
GatsbyNetlifyReactTypeScriptTailwind CSS
© Copyright 2008-2021 Terry Butler