Constructing your First PowerShell Provider

Introduction

I think it's safe to say that PowerShell will be a staple on the Microsoft platform for the foreseeable future. Version 2.0 is shipping standard with Windows 7 and Windows Server 2008 R2. Exchange leverages PowerShell, SQL Server 2008 leverages Powershell, and future server products will be leveraging PowerShell. If you're not building a product's administration features on top of PowerShell you soon will be.

There is, however, a PowerShell learning curve. As a developer you need to master two major PowerShell concepts. The first is familiarity with the PowerShell environment. The second is learning how to build CmdLets and Providers, the components that plug into PowerShell. Much has already been written about building CmdLets. So, in this article, I'm going to demonstrate some basic Provider construction.

Provider Overview

A complete introduction to PowerShell and PowerShell Providers is beyond the scope of this article. So I'll focus on a few of the major PowerShell Provider concepts.

First, I think of a Provider as a storage place for an application's data. A Provider plugs into PowerShell and through the standard or custom CmdLets navigates and modifies the application's data. Typically a Provider is represented by a path which looks similar to the one you would see from the Windows command line. Most of the Microsoft Server products have some type of Provider.

Like CmdLets, Providers can emit .NET Objects. Because Providers and CmdLets are using the Common Language Runtime and the .NET Framework, objects from different providers can interact in new and interesting ways. PowerShell commands can be joined through a Pipeline, so the results of a particular command can feed the input to another command. When you consider the ecosystem of PowerShell providers already shipping with many Microsoft products and the extensibility provided by the .NET Framework; it shouldn't be hard to justify PowerShell support for your product.

Sample Overview

I developed the sample in Visual Studio 2008 and targeted PowerShell 1.0.

Like any software application a Provider's complexity depends on the problem it solves and how the provider solves the problem. The first step to building a provider is to understand how CmdLets interacting with the Provider are translated into function calls in your code. This sample arose from a need to study the Cmdlet to Provider to code interaction.

Aside from dishing up .NET objects, a Provider is responsible for validating the path information input by the user. The sample does some simple validation and emits Win32 debugging information you can view inside of the DebugView utility. Sources at the end of the article include DebugView information for downloading DebugView. The animation below shows the sample code in action.


Figure 1: PowerShell Sample in action

There are many styles of Providers. Styles are in a hierarchy of Provider functionality. The hierarchy appears below.

  • DriveCmdLetProvider
  • ItemCmdLetProvider
  • ContainerCmdLetProvider
  • NavigationCmdLetProvider

The lowest level of functionality is a Drive Provider and the highest is a Navigation Provider. I chose a Navigation Provider so I could explore all possible functionality. Since each level of Provider must supply all the functionally of the Provider a level up, I partitioned the sample code into regions so you can browse all the functions related to a particular Provider in the hierarchy. Later in the article I'll refer to the regions.

Before looking at Navigation Provider code, I want to explain how I utilized DebugView.

Trace and DebugView

.NET Trace and Debug statements are great for troubleshooting real-time software. Basically, they're useful anytime it's not feasible to step through an application with the Debugger. I built the following class to handle writing Trace statements targeted for DebugView.

      public class TraceTest
      {
  ...
          public TraceTest(string area, string application)
          {
              _application = application;
              _area = area;
          }
  
          public void WriteLine(string application, string area, string message)
          {
              if (TraceTest.Activated)
              {
                  Trace.WriteLine(application + ":" + area + ":" + message);
              }
          }
          public void WriteLine(string area, string message)
          {
              this.WriteLine(_application, area, message);
          }
          public void WriteLine(string message)
          {
              this.WriteLine(_application, _area, message);
          }
      }

Though PowerShell has its own Trace and Debug statements, I realized that I wouldn't always have the foresight to activate PowerShell verbose mode. I also realized that what I display in PowerShell may be different than what I want to display when I'm debugging. In addition, I thought it would be faster to do unit testing from a Console Application and hook my code into PowerShell once I had my code debugged. Therefore, I wouldn't always be running from within PowerShell. Finally, with DebugView filtering, I felt I had more selective control over what was displayed.

With some background on PowerShell Providers and some of the tools I used to debug my Provider, I'm ready to explain how the sample provider works.

Constructing your First PowerShell Provider

Getting Started and Drive

Like all .NET development PowerShell development starts with referencing a set of assemblies. System.Management.Automation contains most of what you'll need for building PowerShell components. System.Configuration.Install is another useful assembly I'll discuss later in the article.

In the sample, the Provider is implemented in the TestPSProvider class. As I stated earlier, I'm implementing a Navigation Provider so the base class inherits from NavigationCmdLetProvider. Aside from selecting a Provider base class, a Provider class requires the CmdLetProvider attribute. Below is the Provider class definition.

[CmdletProvider("TestPSProvider", ProviderCapabilities.None)]
public class TestPSProvider : NavigationCmdletProvider
{

PowerShell Provider state is stored in a Drive. Drives typically inherit from PSDriveInfo. The sample Drive class is called TestDriveInfo. The TestDriveInfo class appears below.

  internal class TestDriveInfo : PSDriveInfo
  {
      private TraceTest _trace = new TraceTest("PS");
  
      public TraceTest TraceMessage
      {
          get { return _trace; }
      }
  
      public TestDriveInfo(PSDriveInfo driveInfo)
          : base(driveInfo)
      {
          string somethingToSave = "";
          somethingToSave = "Something";
  
          _trace.WriteLine("PSDriveInfo " + driveInfo.Description + " " + driveInfo.Root + " " + driveInfo.Name + " " + driveInfo.Provider.Name + " " + driveInfo.Provider.Description);
          this.SomethingToSave = somethingToSave;            
      }
  
      public string SomethingToSave { get; protected set; }
  
  }

Aside from storing state, the Drive can be considered the root of a Path. Drive naming choices appear in PowerShell. To create drives, a PowerShell Provider must override InitializeDefaultDrives, the function appearing below.

  protected override System.Collections.ObjectModel.Collection<PSDRIVEINFO> InitializeDefaultDrives()
  {
      System.Collections.ObjectModel.Collection<PSDRIVEINFO> col = new System.Collections.ObjectModel.Collection<PSDRIVEINFO>();
  
      TraceTestPSOut("BEGIN InitializeDefaultDrives");
  
      PSDriveInfo info = new PSDriveInfo("TestPS", this.ProviderInfo, _pathSeparator, "Default Root", PSCredential.Empty);
  
      col.Add(NewDrive(info));
  
      TraceTestPSOut("END InitializeDefaultDrives");
  
      return col;
  }

Most of the really interesting parts of a PowerShell Provider relate to the "item" CmdLets. Item CmdLet processing is handled in the sample code by the functions in the Item and Container regions.

Item and Container

Run the "get-command" Cmdlet and you'll notice a lot of CmdLets ending in "-Item", a few examples are: New-Item, Copy-Item, and Clear-Item. Invoking these CmdLets changes data in the Provider calling similarly named functions among the Item and Container Regions in the Sample application. An annotated view of the Item and Container regions appears below.

  protected override bool ItemExists(string path)
  
  protected override void ClearItem(string path)
  
  protected override void GetItem(string path)
  
  protected override void SetItem(string path, object value)
  
  protected override void CopyItem(string path, string copyPath, bool recurse)
  
  protected override void GetChildItems(string path, bool recurse)

Earlier I mentioned the hierarchy of Providers. The Item region corresponds with functions on the ItemCmdLetProvider and the Container region corresponds to the ContainerCmdLetProvider. The remaining functionality is supplied by the Navigation region functions. Before I cover Navigation I want to briefly touch on Dynamic Parameters.

Constructing your First PowerShell Provider

Navigation and Dynamic Parameters

One interesting PowerShell Provider feature is Dynamic Parameters. If you peruse the NavigationCmdLetProvider object in the object model viewer, you'll notice a "DynamicParameter" function for almost every standard override function. DynamicParameters allow a developer to extend the number of parameters for the standard CmdLets. For example, normally if you want to invoke the New-Item Cmdlet the data is supplied in the - value parameter separated by commas like in the example below.

  New-Item -value one, two, three

DynamicParameters allow a developer to create named parameters rather than a series of values separated by commas. So, you can invoke the New-Item CmdLet on the sample with the following PowerShell code.

  New-Item -MyParameter1 one -MyParameter2 two

Below is the NewItemDynamicParamer function followed by the NewItem function.

  protected override void NewItem(string path, string itemTypeName, object newItemValue)
  {
      TraceTestPSOut("BEGIN NewItem");
  
      if (newItemValue == null)
      {
          TraceTestPSOut("newItemValue is NULL");
      }
      else
      {
          TraceTestPSOut(path + " itemTypeName " + itemTypeName + " value " + newItemValue.ToString());
  
          object[] parms = (object[])newItemValue;
  
          foreach (object obj in parms)
          {
              TraceTestPSOut("Obj " + obj.GetType().ToString() + " " + obj.ToString());
          }
      }
  
      if (this.DynamicParameters == null)
      {
          TraceTestPSOut("DynamicParameters == NULL");
      }
      else
      {
          foreach (RuntimeDefinedParameter objParm in ((RuntimeDefinedParameterDictionary)this.DynamicParameters).Values)
          {
              TraceTestPSOut(" RunTimeDefinedParm " + objParm.Name + " " + objParm.Value.ToString());
          }
      }
  
      TraceTestPSOut("END NewItem");
  }
  
  protected override object NewItemDynamicParameters(string path, string itemTypeName, object newItemValue)
  {
      TraceTestPSOut("BEGIN NewItemDynamicParameters");
  
      RuntimeDefinedParameterDictionary dic = new RuntimeDefinedParameterDictionary();
      ParameterAttribute attrib = null;
      Collection<ATTRIBUTE> col = null;
      RuntimeDefinedParameter runDefParm = null;
  
      attrib = new ParameterAttribute();
      attrib.ParameterSetName = "MyParameters";
      attrib.Mandatory = false;
      attrib.ValueFromPipeline = false;
      col = new Collection<ATTRIBUTE>();
      col.Add(attrib);
  
      runDefParm = new RuntimeDefinedParameter("MyParameter1", typeof(string),
      col);
      dic.Add("MyParameter1", runDefParm);
  
      runDefParm = new RuntimeDefinedParameter("MyParameter2", typeof(string),
      col);
      dic.Add("MyParameter2", runDefParm);
  
      TraceTestPSOut("END NewItemDynamicParameters");
  
      return dic;
  }

Because I chose to accept how PowerShell handles general path validation in a Provider, there was not much to I needed to override in the Navigation region of the sample.

That covers all the Provider functions. There is some needed code though to get the sample to work in PowerShell.

Attributes and Installation

To load your Provider you need a PSSnapIn class. Here is the TestPSProviderPSSnapIn class from the sample.

  [RunInstaller(true)]
  public class TestPSProviderPSSnapIn : PSSnapIn
  {
      /// <SUMMARY>
      /// Create an instance of the TestPSProviderPSSnapIn
      /// </SUMMARY>
      public TestPSProviderPSSnapIn()
          : base()
      {
      }

Other than providing some naming properties there is not much to change in this class. Note however, the Installer Attribute and the System.Configuration.Install assembly reference is necessary to enable InstallUtil.exe functionality.

The PowerShell code below loads the SnapIn and installs the Provider. It's been configured to run the 64 bit version of InstallUtil.

  $path = Resolve-Path "C:\articles and presentations\PowerShell\PowerShell\Testing.PowerShell.Basics\bin\Debug\Testing.PowerShell.Basics.dll"
  $register = "$env:windir/Microsoft.NET/Framework64/v2.0.50727/installutil"
  & $register $path
  add-PSSnapin TestPSProviderPSSnapIn
  set-location TestPS:\Container 

Conclusion

A PowerShell Provider is a lot like the database of a PowerShell solution. Standard CmdLets act on the Provider, adding, removing, and reading data stored in the Provider. To understand how to build a Provider, a developer must understand how, when, and where .NET Provider code gets executed. This article and the accompanying sample code demonstrates .NET code execution in a Navigation Provider.

Resources

Designing Your Windows PowerShell Provider
How to Create a Windows PowerShell Provider
Windows PowerShell Blog
Windows PowerShell Getting Started Guide DebugView



About the Author

Jeffrey Juday

Jeff is a software developer specializing in enterprise application integration solutions utilizing BizTalk, SharePoint, WCF, WF, and SQL Server. Jeff has been developing software with Microsoft tools for more than 15 years in a variety of industries including: military, manufacturing, financial services, management consulting, and computer security. Jeff is a Microsoft BizTalk MVP. Jeff spends his spare time with his wife Sherrill and daughter Alexandra.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds