Coordinating .NET Framework Custom PowerShell Providers, Pipelines, and Custom Cmdlets

Introduction

In a prior article, Constructing your First PowerShell Provider, we covered PowerShell Provider development. In my follow-up article, Debugging and Designing Custom .NET Framework PowerShell Providers, we looked at more advanced PowerShell development topics like Provider design and debugging. Both articles focused mostly on providers. Most solutions, however, will likely include Custom CmdLets that access data in the PowerShell Pipeline and work in conjunction with a custom provider.

In this article I want to "close the loop", so to speak, between Custom Providers and Custom CmdLets. I'm going to demonstrate how Custom CmdLets work with a Customer Provider and then how the components leverage the PowerShell Pipeline.

Overview

A complete introduction to PowerShell CmdLets, providers, and the PowerShell Pipeline are beyond the scope of this article. However some context is needed to understand the technical issues.

A PowerShell Provider serves as a sort of data source. Like a database data source a PowerShell programmer can navigate a provider using standard conventions and a standard set of operations. CmdLets are the operations a programmer applies to a provider. A standard set of providers and CmdLets ships with PowerShell, but a developer is free to build Custom CmdLets and providers. CmdLets often write their results to the PowerShell Pipeline and pull data out of the PowerShell Pipeline. The PowerShell Pipeline enables chaining segments of CmdLet operations together, receiving data from the Pipeline, writing data to the Pipeline, or reading and writing to the Pipeline.

Because all CmdLets and Providers are built upon the .NET Framework, objects emitted by the standard set of CmdLets can be consumed by Custom CmdLets and vice-versa.

The accompanying sample applications demonstrate how the Provider, CmdLet, and Pipeline interaction works. Leveraging a standard Windows Event Log CmdLet, the sample:

  • Navigates a Provider
  • Reads the Application Event Log using the standard PowerShell CmdLet
  • Packages the Event Log data and writes the data to the Pipeline
  • Reads the data from a Pipeline and invokes a CmdLet which saves the Event Log information to the Provider

The full PowerShell code appears below:

  add-PSSnapin PSCoreProviderPSSnapIn
  #Navigate to a place in the Provider
  set-location Test:Children1
  #Read Event Log data, package the data, and write the packaged data to the current Provider
  Get-Eventlog -LogName Application -EntryType Information -Newest 5 | New-EventLogItem -SendCollection $false | Set-EventLogItem
  Get-Eventlog -LogName Application -EntryType Information -Newest 5 | New-EventLogItem -SendCollection $true | Set-EventLogItem


Figure 1

In the code above, Get-EventLog is a standard PowerShell CmdLet which reads events from the event Log and writes the information to the PowerShell Pipeline. Set-Location is a standard PowerShell CmdLet utilized to navigate a provider. The other CmdLets and providers are custom code from the sample.

There is a subtle difference between lines 5 and 6 that demonstrate different ways a CmdLet can send data to the pipeline. I'll cover those differences later in the article. First, though, I'll briefly cover building a custom CmdLet before delving into how the sample code executes.

Custom CmdLet Primer

A complete introduction to building a custom CmdLet is beyond the scope of this article. As I stated earlier though, some context is needed to understand the sample.

The code below differentiates a CmdLet from another .NET framework class.

  [Cmdlet(VerbsCommon.Set, "EventLogItem", SupportsShouldProcess = false)]
  public class PSSetEventLog : PSCmdlet

CmdLets typically derive from PSCmdLet and are decorated with the Cmdlet attribute. The CmdLet attribute describes the CmdLet to PowerShell. When a CmdLet is invoked by PowerShell, CmdLet methods are called in the following order.

  • BeginProcessing method is called when the CmdLet is first activated
  • ProcessRecord is invoked for each piece of data in the Pipeline
  • EndProcessing is called when CmdLet Activation ceases

CmdLets override one or more methods mentioned above. CmdLet parameters are expressed as class properties decorated with parameter attributes. A sample CmdLet parameter from the PSNewEventLog class appears below:

  [Parameter(Mandatory = true, HelpMessage = @"Determines how to process pipeline values", ValueFromPipeline = false)]
  public bool SendCollection { get; set; }

When the PowerShell script runs, New-EventLogItem is the first custom CmdLet invoked in the sample code. PSNewEventLog is the class associated with the CmdLet. PSNewEventLog receives data from the pipeline and sends data to the pipeline using one of two different methods. Later in the article I'll explain the different methods, first I'll explain how the CmdLet pulls data from the pipeline.



Coordinating .NET Framework Custom PowerShell Providers, Pipelines, and Custom Cmdlets

New-EventLogItem - From the Pipeline

CmdLets receiving data from the pipeline must have a property setup similar to the PSNewEventLog class properties below.

  [Parameter(Mandatory = false, HelpMessage = @"EventLogEntry object used with Get-EventLog", ValueFromPipeline = true)]
  public EventLogEntry LogEntry { get; set; }
  
  [Parameter(Mandatory = false, HelpMessage = @"EventLogRecord used with Get-WinEvent", ValueFromPipeline = true)]
  public EventLogRecord LogRecord { get; set; }

PowerShell sets the property to data from the pipeline when the ValueFromPipeline parameter is set to "true" and when the type of data matches the data in the pipeline. In the example above, two parameters accept different information because there are two standard PowerShell CmdLets for getting Events from the Event Log. Get-EventLog sends EventLogEntry classes to the pipeline and Get-WinEvent sends EventLogRecord classes to the pipeline.

Features built into PowerShell perform all the class matching so there are no type mismatch errors when a property is set. Most of the remaining PSNewEventLog class code appears below:

  private List<Info_EventData> _data = new List<Info_EventData>();
  
  protected override void ProcessRecord()
  {
      var list = new List<object>();
      Info_EventData eventData = null;
  
      if (this.LogEntry != null) { eventData = new Info_EventData(this.LogEntry); }
  
      if (this.LogRecord != null) { eventData = new Info_EventData(this.LogRecord); }
  
      if (eventData == null)
      {
      }
      else
      {
          if (this.SendCollection)
          {
              _data.Add(eventData);
          }
          else
          {
              this.WriteObject(eventData, false);
          }
      }
  }
  
  protected override void EndProcessing()
  {
      if (this.SendCollection)
      {
          this.WriteObject(_data, false);
      }
  }

The presence of data in the pipeline results in the following set of operations on the PSNewEventLog class:

  • The appropriate class Property is Set.
  • ProcessRecord is then invoked.

In the PowerShell script, after New-EventLogItem sends data to the pipeline, the Set-EventLogItem picks the data from the pipeline and saves it to the current provider. Set-EventLogItem is mapped to the PSSetEventLog class.

PSSetEventLog class works in tandem with PSNewEventLogItem receiving data from the Pipeline that PSNewEventLogItem has passed to the Pipeline.

As I stated earlier, New-EventLogItem interacts with the pipeline in different ways dependent on the SendCollection parameter. When SendCollection is "false" each event is posted separately to the pipeline. If SendCollection is "true" the events are collected from the pipeline and then posted to the pipeline as a collection of Info_EventData classes.

New-EventLogItem and Set-EventLogItem in Tandem

When Info_EventData objects are posted separately to the pipeline, it's easiest to see the interaction between the New-EventLogItem, Set-EventLogItem, and the pipeline by running the sample in the debugger. Code breakpoints oscillate between the following method and code highlighted in red below:

On the PSNewEventLog class:

  protected override void ProcessRecord()
  {
      var list = new List<object>();
      Info_EventData eventData = null;
  
      if (this.LogEntry != null) { eventData = new Info_EventData(this.LogEntry); }
  
      if (this.LogRecord != null) { eventData = new Info_EventData(this.LogRecord); }
  
      if (eventData == null)
      {
      }
      else
      {
          if (this.SendCollection)
          {
              _data.Add(eventData);
          }
          else
          {
              this.WriteObject(eventData, false);
          }
      }
  }
   
  On the PSSetEventLog class
  protected override void ProcessRecord()
  {
      foreach (var eventData in Data)
      {
          _drive.SaveEventInfo(eventData);
      }
  }

As the sample executes PowerShell sets the data property in PSSetEventLog class to an array of Info_EventLogs with a single item. As I stated earlier, PowerShell attempts to coerce the pipeline data into something palatable to the CmdLet.

Pipeline Enumeration

As stated ealier, if SetCollection is "true" New-EventLogItem collects all the events and posts the data as a collection of Info_EventLog classes. Instead of writing separate events to the PowerShell pipeline, the PSNewEventLog class collects the Info_EventLog classes in a list. As CmdLet activity ceases the EndProcessing method is invoked.

  protected override void EndProcessing()
  {
      if (this.SendCollection)
      {
          this.WriteObject(_data, false);
      }
  }

Calling WriteObject adds the whole collection to the PowerShell pipeline. Control flows to the PSSetEventLog class and PowerShell sets the data property on the PSSetEventLog class to a collection of multiple Info_EventLog classes rather than a collection with a single item like in the "false" example above. As you may have noticed the second parameter to the WriteObject dictates whether a collection is enumerated. Had the second parameter been true, PowerShell would have iterated through the collection and sent separate objects to the pipeline, invoking ProcessRecord on PSSetEventLog multiple times.

Each Info_EventLog class is saved to the TestDriveInfoClass contained in the sample's PowerShell Provider.

Finding the Provider

The following code in the PSSetEventLog class sets the _drive variable that the ProcessRecord method uses to save the Info_EventLog data:

  protected override void BeginProcessing()
  {
      var location = this.CurrentProviderLocation("TestProvider");
      _drive = (TestDriveInfo)location.Drive;
  
  }

Properties on the PSCmdLet base class furnish access to the current provider. "TestProvider" is the provider name embedded in the CmdLetProvider attribute decorating the PSCorePartsProvider class here is the sample code:

  [CmdletProvider("TestProvider", ProviderCapabilities.None)]
  public class PSCorePartsProvider : NavigationCmdletProvider

Conclusion

Custom PowerShell solutions often include a mix of custom provider, custom CmdLets, and various PowerShell pipeline behaviors. While the PowerShell infrastructure performs a lot of the mapping work and furnishes a CmdLet with access to the current provider there are areas requiring a .NET developer's attention particularly in how a CmdLet leverages the pipeline. Subtle adjustments in the pipeline behavior can greatly influence the architecture of a solution.

Resources

Creating a Windows PowerShell cmdlet with a scriptblock parameter
How PowerShell Works - this is a great overview of how the RunSpace and pipeline work
Extend Windows PowerShell with Custom Commands
Pipelining: Definition

Related Articles





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.

Downloads

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: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • On-demand Event Event Date: October 29, 2014 It's well understood how critical version control is for code. However, its importance to DevOps isn't always recognized. The 2014 DevOps Survey of Practice shows that one of the key predictors of DevOps success is putting all production environment artifacts into version control. In this webcast, Gene Kim discusses these survey findings and shares woeful tales of artifact management gone wrong! Gene also shares examples of how high-performing DevOps …

Most Popular Programming Stories

More for Developers

RSS Feeds