Coding Stephan

Using dependency injection in your C# PowerShell Modules

For those following along, this is the second post in a series about PowerShell development in C#. In the first post I showed you how to create a binary PowerShell module in C# and how to debug it in Visual Studio. This post will continue this journey and show you how to use dependency injection in your binary PowerShell module.

Dependency injection and async code in PowerShell

Series: PowerShell Development

Dependency Injection

All C# developers I know are using more or less dependency injection in their code. It’s a great way to organize your code and to make classes just responsible for their own things. I’m not going to explain dependency injection in this post, there are a lot of great resources out there that can explain it better than I can. Like Dependency Injection in ASP.NET core

In PowerShell there is no such thing as dependency injection (built-in), but there are some ways to do it. As a seasoned C# developer I was disappointed to find out that PowerShell does not support any dependency injection out of the box. I was not going to let that stop me from using dependency injection in my binary PowerShell modules.

Asynchronous code

Another disappointment was the fact that PowerShell does not support asynchronous code, we all love the async and await keywords in C#, right? Out-of-the-box there is no support for that as well. And even if you try to just use asyncMethod().GetAwaiter().GetResult() you’ll find out that it’s not supported either, writing anything to the PowerShell streams will throw an exception.

Introducing Svrooij.PowerShell.DependencyInjection

For WinTuner I really wanted to create a PowerShell module, instead of a Command Line Interface (CLI) tool it currently is. But I still wanted support for (at least):

  • Dependency Injection
  • Asynchronous code
  • ILogger<T> support, that integrates with PowerShell

After checking some of the available options I figured out there is no such library offering all these features. So there you have it, I created a library that does all these things. It’s called Svrooij.PowerShell.DependencyInjection and it’s available on NuGet.

Using this library

Let’s start by creating a new PowerShell module, you can follow the steps in the first post to create a new PowerShell module. This builds on top of that post, so you should have a working PowerShell module before continuing.

Add the NuGet package

dotnet add package Svrooij.PowerShell.DependencyInjection

Create a Startup class

Create a new class that is going to be the place where you register your dependencies. I called it Startup but you can call it whatever you want.

// It has to be a public class with `PsStartup` as base class
public class Startup : PsStartup
{
    // Override the `ConfigureServices` method to register your own dependencies
    public override void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<ITestService, TestService>();
    }

    // Override the `ConfigurePowerShellLogging` method to change the default logging configuration.
    public override Action<PowerShellLoggerConfiguration> ConfigurePowerShellLogging()
    {
        return builder =>
        {
            builder.DefaultLevel = LogLevel.Information;
            builder.LogLevel["Svrooij.PowerShell.DependencyInjection.Sample.TestSampleCmdletCommand"] = LogLevel.Debug;
            builder.IncludeCategory = true;
            builder.StripNamespace = true;
        };
    }
}

Change your PSCmdlet

Change your command lets to inherit from DependencyCmdlet<Startup> instead of PSCmdlet.

I purposely changed the visibility for BeginProcessing(), StopProcessing(), EndProcessing() and ProcessRecord() to protected sealed so you can no longer override those, they are used by the library to execute various things when called by PowerShell.

In your command let with the DependencyCmdlet<Startup> base class, there is one method that you can override, and that is the ProcessRecordAsync(...) method. This method will be called internally by the ProcessRecord() method, and the provided CancellationToken should be used in all async calls. It’s is triggered if the user presses CTRL + C during the execution of your command let, by passing this token to all async calls, you make sure the code is actually cancellable by the user.

public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
{    
    // This would normally fail from the async method, but now it's handled by the library.
    WriteWarning("This is a warning");
    
    // Use WriteObject right from the async method, without PowerShell complaining that it's not on the right thread.
    WriteObject(new FavoriteStuff
    {
        FavoriteNumber = this.FavoriteNumber,
        FavoritePet = this.FavoritePet
    });
}

Use your dependencies

Dependency injection was the main thing missing, construction injection would be my to preference. That would require changing the runtime, so I opted for resolving the dependencies based on reflection. This means all private and internal properties and fields marked with the ServiceDependency attribute will be resolved by the library, this is done when PowerShell calls the BeginProcessing() method (which is why it’s sealed).

[Cmdlet(VerbsDiagnostic.Test, "SampleCmdlet")]
public partial class TestSampleCmdletCommand : DependencyCmdlet<Startup>
{
  // Add one ore more properties or fields with the `ServiceDependency` attribute.
  // The library will resolve these dependencies for you when PowerShell calls the `BeginProcessing()` method.
  [ServiceDependency]
  internal ITestService TestService { get; set; }
  
  public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
  {
    // Call an async method on your dependency, and pass the `CancellationToken` to it.
    var result = await TestService.GetFavoriteStuffAsync(cancellationToken);
    WriteObject(result);
  }
}

Logging

When you’re used to using dependency injection, you’re probably also using the ILogger<T> interface to centralize logging. This library has full support for that, and it’s automatically integrated with PowerShell. So logger.LogError() will end up in the PowerShell error stream, and logger.LogInformation() will end up in the regular console output.

[Cmdlet(VerbsDiagnostic.Test, "SampleCmdlet")]
public partial class TestSampleCmdletCommand : DependencyCmdlet<Startup>
{
  // Resolve the ILogger<T> with the `ServiceDependency` attribute.
  // The library automatically registers the correct `ILoggerProvider` for you.
  [ServiceDependency]
  internal ILogger<TestSampleCmdletCommand> logger;
  
  public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
  {
    logger.LogInformation("Starting to process the record");
  }
}

The correct LoggerProvider is automatically registered for you, so any dependencies that use the ILogger<T> interface will automatically log to PowerShell, which you can configure if you want.

Why did I create this library?

My new pet project WinTuner is a tool to package installers from WinGet into .intunewin files and to upload them to Intune. Currently it’s a .NET command line tool, since this tool is mostly for system Administrators, who do not have the .NET SDK installed, I wanted to create a PowerShell module for it. This way they can just install the module from the PowerShell Gallery and use it.

The deep desire to always learn new things, made me want to experiment with creating a PowerShell module for this. I figured out you can build binary PowerShell modules (with C#), but I was missing some key parts to get me going. This library fills those gaps for me, and I hope it will enable you to create your own binary PowerShell modules as well.

Binary PowerShell modules are possibly much more performant then regular PowerShell modules. In the end it’s all converted to C# at some point by the PowerShell runtime, so why not just write it in C# in the first place? And with me already having all the important code build in C#, calling that code from this new binary PowerShell module (with ILogger and dependency injection support) is a breeze. I must admit I start to love the PowerShell mechanics, where it auto completes the Commands and Parameters for you.

Boo, reflection

I know, I know, reflection is slow, and I’m using it to resolve the dependencies. But I’m not using it in the critical path, it’s only used when PowerShell calls the BeginProcessing() method. So it’s only used once per command let, and not for every call to the command let. I’m not saying it’s fast, but it’s not that slow either.

I’m also experimenting with a source generator to generate a method that will resolve the dependencies for that class, without using reflection. But I’m having some issues with that. I’m not sure if I’m going to continue that path, but it’s an interesting experiment.

What’s next?

I’m going to use this library to create a PowerShell module for WinTuner, while doing that I figured out the next challenge.

Documenting binary PowerShell modules is kind of a hassle right now, as a developer I’m used to documenting libraries with XML comments. But PowerShell has yet another slightly different help file format. Most regular PowerShell modules are manually documented with the use of platyPS. I’m going to try to automate this process, so I can just use the XML comments to generate the help files, but that’s for another post.