Does this site look plain?

This site uses advanced css techniques

When I first started building cmdlets to serve an important business need at a customer, one of my early wishes was to have some concept of an ongoing session, where if I made a connection or made something current, that setting would "stick" and be available for cmdlets that followed.

For years I never dug in enough to really sort this out, but recently have found the right way, and it's easy!

The key is the SessionState.PSVariable property that's available to all cmdlets derived from PSCmdlet (but not from Cmdlet), and we can fetch/store arbitrary objects by name.

These objects can be individual items, but my approach is to create a state object that goes into a single named variable; it simplifies my use case, but others might want to group things differently.

With this, we'll be able to set a configuration at the start of doing our work, not needing to provide them to the cmdlets that follow.

Our Custom State Object

The first step is to create the class that will hold the state, and we're intentionally using a really simple example. These items will be presumably configured by the user at the start of some session, and future cmdlet invocations can pull this saved information if not provided explicitly on the command line:

public class MyState {
  public string URL      { get; set; }
  public string Username { get; set; }
}

I typically define the state class itself inside the cmdlet source area, but the object can contain other objects defined (and more widely used) elsewhere.

Bad idea: It's a poor practice to store sensitive data (such as passwords) directly into SessionState; instead use either SecureString or PSCredential objects to hold them. Ref: Working with Passwords, Secure Strings and Credentials in Windows PowerShell.

Fetch/Store State Variables

The PSCmdlet.SessionState.PSVariable object has Set() and GetValue() methods, both of which take a name and an arbitrary object: the former is obvious, while the latter either fetches the object if found by name in session state, or it returns the default value provided in the second argument (which is often null).

Though it's possible to do the variable manipulation inside each cmdlet's parameter processing, it's far easier to put this in the base interface class all your cmdlets were derived from (which in turns derives from PSCmdlet; see my previous post for how I usually do this).

public abstract class MyCmdlet : PSCmdlet {
   ...
   private const string VarName = "_MyState";

   protected MyState getState()
   {
       var state = SessionState.PSVariable.GetValue(VarName, null) as MyState;

       if (state == null)
           SessionState.PSVariable.Set(VarName, state = new MyState());

       return state;
   }
   ...
}

This creates a new (and empty) MyState object the first time it's called and stores it into session state, returning that same value every time thereafter. This doesn't know anything about the specific values of items within the state object, that's for the caller to fool with.

I don't see any way to enumerate the variable names stored in session state, so it appears that you have to know the name of anything you want to work with.

Because the GetVariable()

What I don't know:: Does this session state apply only to the currently-loaded module that contains the cmdlets, or is this global for the entire PowerShell session? I need to find out.

Initialize state up front

We still have to populate our session state, and this requires an explicit first step. There are a number of ways, but I typically use an explicit Initialize-MyState cmdlet:

[Cmdlet(VerbsData.Initialize, "MyState")]
public class Initialize_MyState : MyCmdlet {

  [Parameter(Mandatory = false)]
  public string URL {get; set;}

  [Parameter(Mandatory = false)]
  public string Username {get; set;}

  protected override void ProcessRecord()
  {
    base.ProcessRecord();

    bool any = false;

    if (URL != null)
    {
      getState().URL = URL;
      any = true;
    }

    if (Username != null)
    {
      getState().Username = Username;
      any = true;
    }

    if ( !any)
    {
      // ERROR: you need to provide *something*
    }
    else
    {
      WriteVerbose("Session state variables initialized");
    }
  }
}
Bad idea: this module demonstrates a poor practice on handling a number of parameters, where all of them are optional but at least one has to be provided. This could probably be done with ParameterSets rather than the messy any test, but this poor mechanism is nevertheless far more easier to follow.
Coming Soon: we'll provide better guidance on error handling in Powershell; there's a lot of hand-waving about "Error occurs here" on this page.

With this cmdlet in place, we can load the assembly into our PowerShell session and tell it to use a certain URL for the remainder of the session:

PS> Import-Module Blog.Cmdlet.dll
PS> Initialize-MyState -URL http://unixwiz.net

Once initialized, this information hangs around and is available for future cmdlets.

Pulling from session state

Now that session state is available and possibly initialized, it's time to use it. There are lots of possible use cases, but in mine, each cmdlet supports all parameters on the command line, but will pull from session state if the user didn't provide something.

[Cmdlet(VerbsCommunications.Connect, "Server")]
public class Connect_Server : MyCmdlet
{
  [Parameter(Mandatory = false)]
  public string URL {get; set;}

  protected override void BeginProcessing()
  {
    base.BeginProcessing();

    if (URL == null) URL = getState().URL; 

    if (URL == null)
    {
       // Throw error; missing URL parameter
    }
  }

  protected override void ProcessRecord()
  {
    // DO THE REAL WORK
  }
}

We use BeginProcessing() to pull URL from session state only if the -URL parameter was not provided by the caller, and it's an error if the URL is not found one way or the other.


First published: 2019/07/05

The excellent icons on this page were made by Freepik from www.flaticon.com, and are licensed by CC 3.0 BY