Freddy The Horse
Freddy The Horse

Reputation: 345

C# API library and logging to PowerShell's WriteVerbose()

I'm building a DLL in C# that I will be consuming with several different projects - so far, I know of a WPF application and a (binary) PowerShell module. Because the core business logic needs to be shared across multiple projects, I don't want the PowerShell module itself to contain the core logic. I'd just like to reference my primary library.

I'm struggling to figure out how to implement a clean logging solution in my core DLL that will be accessible via PowerShell's WriteVerbose() method. Without this, I can provide verbose output to PowerShell about PowerShell-specific things, but I can't provide any verbose output about "waiting for HTTP request" or other features that would be in the core DLL.

Here's a simple example of what I'm trying to do:

using System;
using System.Threading;

namespace CoreApp
{
    public class AppObject
    {
        public AppObject() {}

        public int DoStuffThatTakesForever()
        {
            // Assume logger is a logging object - could be an existing
            // library like NLog, or I could write it myself

            logger.Info("Doing step 1");
            Thread.Sleep(5000);

            logger.Info("Doing step 2");
            Thread.Sleep(5000);

            logger.Info("Doing step 3");
            Random r = new Random();
            r.Next(0, 10);
        }
    }
}

////////////////////////////////////////////////////////////
// Separate VS project that references the CoreApp project

using System.Management.Automation;
using CoreApp;

namespace CoreApp.PowerShell
{
    [Cmdlet(VerbsCommon.Invoke, "ThingWithAppObject"]
    [OutputType(typeof(Int32))]
    public class InvokeThingWithAppObject : Cmdlet
    {
        [Parameter(Position = 0)]
        public AppObject InputObject {get; set;}

        protected override void ProcessRecord()
        {
            // Here I want to be able to send the logging phrases,
            // "Doing step 1", "Doing step 2", etc., to PowerShell's
            // verbose stream (probably using Cmdlet.WriteVerbose() )
            int result = InputObject.DoStuffThatTakesForever();

            WriteObject(result);
        }
    }
}

How can I provide verbose PowerShell verbose output without tightly binding the core library with the PowerShell module?

Upvotes: 2

Views: 1108

Answers (1)

Freddy The Horse
Freddy The Horse

Reputation: 345

I'm definitely open to other solutions, but here's how I ended up solving it:

In the core library, I created an ILogger interface with methods for Info, Verbose, Warn, etc. I created a DefaultLogger class that implemented that logger (by writing everything to the attached debugger), and I gave this class a static singleton instance.

In each method that I wanted logged, I added an optional ILogger parameter, and added a line to use the default logger if necessary. The method definitions now look like this:

public int DoSomething(ILogger logger = null)
{
    logger = logger ?? MyAppLogger.Singleton;

    // Rest of the code
    Random r = new Random();
    return r.Next(0, 10);
}

I had to do this for each method because the PSCmdlet.WriteVerbose() method expects to be called from the currently running cmdlet. I couldn't create a persistent class variable to hold a logger object because each time the user ran a cmdlet, the PSCmdlet object (with the WriteVerbose method I need) would change.

Finally, I went back to the PowerShell consumer project. I implemented the ILogger class in my base cmdlet class:

public class MyCmdletBase : PSCmdlet, ILogger
{
    public void Verbose(string message) => WriteVerbose(message);

    public void Debug(string message) => WriteDebug(message);

    // etc.
}

Now it's trivial to pass the current cmdlet as an ILogger instance when calling a method from the core library:

[Cmdlet(VerbsCommon.Invoke, "ThingWithAppObject"]
[OutputType(typeof(Int32))]
public class InvokeThingWithAppObject : MyCmdletBase
{
    [Parameter(Mandatory = true, Position = 0)]
    public AppObject InputObject {get; set;}

    protected override void ProcessRecord()
    {
        int result = InputObject.DoSomething(this);
        WriteObject(result);
    }
}

In a different project, I'll need to write some kind of "log adapter" to implement the ILogger interface and write log entries to NLog (or whatever logging library I end up with).

The only other hiccup I ran into is that WriteVerbose(), WriteDebug(), etc. cannot be called from a different thread than the main thread the cmdlet is running on. This was a significant problem, since I'm making async Web requests, but after banging my head on the wall I decided to just block and run the Web requests synchronously instead. I'll probably end up implementing both a synchronous and an async version of each Web-based function in the core library.

This approach feels a bit dirty to me, but it works brilliantly.

Upvotes: 2

Related Questions