Caching AD objects with PowerShell Classes!

Implementing caching in PowerShell is easy, but can we make it more transparent to the user? Let's see how we might use PowerShell to class it up a bit (pun most definitely intended)

Functional but flawed

In the previous post I offered some strategies for caching AD objects in Powershell. The last, and probably most general-purpose example I gave looked like this:

# Save all users to a variable
$Agents = Get-ADUser -Filter "title -like '*agent*'" -Properties manager

# Start out with an empty lookup table
$LookupTable = @{}

foreach($Agent in $Agents){  
  [pscustomobject]@{
    Agent = $Agent.Name
    Manager = if($LookupTable.ContainsKey($Agent.manager){
        $LookupTable[$Agent.manager]
      }
      else{
        ($LookupTable[$Agent.manager] = $(Get-ADUser -Identity $Agent.manager -ErrorAction SilentlyContinue).Name)
      }
  }
}

While functionally optimal, the if statement hurts readability slightly, and distracts from what we're doing (creating an object). Maybe we can abstract away the entire thing somehow...

class noun

\ ˈklas \

PowerShell 5.0 introduced a new native method for generating types. To aid the extensibility of DSC, the PowerShell team introduced the concept of a class, a blueprint for object behavior.

The syntax and grammar borrows heavily from that of C#, and effectively implements a subset of the features found in classes in that language - making classes a perfect steppingstone for both PowerShell users wanting to get into C#, and for C# developers looking to get into PowerShell, yay!


Let's wrap the caching logic up in a class!

The ADUserCache class

The example above should be pretty straightforward to wrap inside a class:

class ADUserCache  
{
  [hashtable]$LookupTable

  ADUserCache(){
    $this.LookupTable = @{}
  }

  [psobject] GetADUser([string]$Identity){
    if($this.LookupTable.Contains($Identity)){
      return $this.LookupTable[$Identity]
    }
    else{
      return ($this.LookupTable[$Identity] = Get-ADUser -Identity $Identity -ErrorAction SilentlyContinue) 
    }
  }
}

With the ADUserCache class defined above, we can simplify the example from before considerably:

# Save all users to a variable
$Agents = Get-ADUser -Filter "title -like '*agent*'" -Properties manager

# Start out with an empty cache, by creating an instance of our ADUserCache class
# We could as well have used 
#   New-Object -TypeName ADUserCache
$UserCache = [ADUserCache]::new()

foreach($Agent in $Agents){  
  [pscustomobject]@{
    Agent = $Agent.Name
    Manager = $UserCache.GetADUser($Agent.manager).Name
  }
}

Much nicer user experience! And the code is (almost) readable again! We can keep our performance gains and have some pretty clean code in our scripts, sweet!

I guess my job here is done...


The real explanation

... okay, maybe that was not completely self-explanatory. Yesterday I saw this tweet from Adam the Automator:


It sometimes feels like that, doesn't it? I think we can do a better job of explaining this. Let's have a look at that "class" definition thing again, with inline comments

class ADUserCache  
{
  # Declare the $LookupTable hash table variable
  # Inside the class methods we can refer to it as $this.LookupTable
  [hashtable]$LookupTable

  # This is our constructor
  # It runs the first time we create an instance of this class
  ADUserCache(){
    # Create the actual hashtable that will store the cached objects
    $this.LookupTable = @{}
  }

  # This GetADUser() method is going to facilitate the actual caching behavior
  [psobject] GetADUser([string]$Identity){
    # Let's see if the user exists in the cache already
    if($this.LookupTable.Contains($Identity)){
      return $this.LookupTable[$Identity]
    }
    else{
      # Oops, a cache miss! Retrieve the user from the directory.
      return ($this.LookupTable[$Identity] = Get-ADUser -Identity $Identity -ErrorAction SilentlyContinue) 
    }
  }
}

So now we have a bit of context around the moving parts, but... why?

Maybe apart from the PowerShell-likeness (The $ prefix and type literals enclosed in []), the structure of the ADUserCache class should look familiar if you have experience with classes in C#, C++, Java or Dart - but if you're new to classes (or unfamiliar with the languages listed above), it might require a bit of clarification, so let's break it down even further.

Basic structure of a PowerShell Class

Let's look at the basic structure of our class first:

class ADUserCache  
{
  # ...
}

At a minimum, a class declaration statement in PowerShell requires 3 syntactical elements:

  • The class key word, followed by
  • A name, in our case ADUserCache, and finally
  • A class body, the block surrounded in {}

Class Names (and Poop Emojis)

The class name specification is a little more strict than the regular PowerShell user might expect. Function names can basically be any unicode string in PowerShell:

A class name, on the other hand, needs to begin with a letter, followed by zero or more letters, numbers or connectors (like _). I haven't found the time to dig out the class parsing logic in the official powershell/powershell GitHub repo, but if we assume the naming conventions mirror those of C#, we can validate class names with a simple (but terribly loooong) regex pattern:

filter isValidClassName {  
  $_ -cmatch '^[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}][\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\p{Cf}]*$'
}

Applying it to couple of potential class name of varying quality, we'll find ADUserCache to be suitable, technically at least:

The Body

What goes into the class? The behavior that we can implement using a PowerShell class come in two varieties: Properties and Methods.

Properties

Properties are a bit like variables - they hold references to data. When we're using classes, we need to declare our class properties by name and associate them with a type before we can use them.

In the ADUserCache class, we only really need one variable to keep state - the $LookupTable, and so we declare it like so:

  [hashtable]$LookupTable

The type literal on the left signifies the type of whatever value we'll assign to $LookupTable. We could have also marked it either hidden or static (or both), but for the sake of simplicity let's just focus on the basics for now.

Methods

Methods are a bit like functions in PowerShell, with a few notable differences. Let's have a look at the GetADUser() method:

  [psobject] GetADUser([string]$Identity){
    # ...
  }

If you want to return anything from a PowerShell method, you need to declare a return type using a type literal, much like we did with the $LookupTable property. Since every object in the PowerShell runtime is transparently wrapped in a PSObject, it'll make a great substitute for "any type".

The method name, GetADUser, is followed by a list of parameters in () - unlike regular functions or scriptblocks a method doesn't take an inline param() block.

If we want to reference a class property from inside the method, we'll need to do so via the $this automatic variable:

# this is a reference to that $LookupTable variable we declared
$this.LookupTable[$Identity] 

This may feel a bit awkward at first, but you'll get used to it. One more thing to take note of...

Notice that as soon as your method has a return type, all code paths in your method must use the return keyword followed by an expression or pipeline that results in an object of that type. Compare this seemingly similar function/method pair below:

function Draw-Lottery(){
  if(Get-Random 0,1){
    return "Win!"
  }
  "Lose!"
}

[string] DrawLottery(){
  if(Get-Random 0,1){
    return "Win!"
  }
  return "Lose!"
}

The primary function of the return keyword is not actually to "return something" - it is to return control to the caller - this is of course a perfect point in time to also pass any output back to the caller.

This is why the Draw-Lottery function works perfectly fine with just a single return statement. PowerShell doesn't care that the function is not ready to return control to the caller, it'll gladly take any output (like the string "Lose!") already and stuff it down the pipeline.

In the DrawLottery() method on the other hand, we are required to specify the output immediately after the return statement. Think of the method signature, [string] DrawLottery() as a code contract - we promise the user that we will pass back an object of type string, and as a form of internal assurance, the parser needs to be able to guarantee that something is returned when the method stops executing.

Constructors

Wait, didn't I say there were only two varieties of things we put into our class body? Well, there's a special type of method that we need to inspect a little closer - a constructor:

  ADUserCache(){
    $this.LookupTable = @{}
  }

A constructor always has the exact same name as the class itself - no way around it. It also cannot have a return type.

Whenever someone creates a new instance of our class, the appropriate constructor will run as the very first thing, before the newly created object is returned to the caller:

This makes it a suitable place to conduct initialization tasks, such as creating the actual hash table that will back our cache.

Conclusion

Now that we hopefully have a better understanding of which elements go into a powershell class declaration, the ADUserCache class should make a bit more sense:

# Class declaration
class ADUserCache  
{
  # Property
  [hashtable]$LookupTable

  # Constructor
  ADUserCache(){
    # Create the actual hashtable that will store the cached objects
    $this.LookupTable = @{}
  }

  # Method
  [psobject] GetADUser([string]$Identity){
    # Let's see if the user exists in the cache already
    if($this.LookupTable.Contains($Identity)){
      return $this.LookupTable[$Identity]
    }
    else{
      # Oops, a cache miss! Retrieve the user from the directory.
      return ($this.LookupTable[$Identity] = Get-ADUser -Identity $Identity -ErrorAction SilentlyContinue) 
    }
  }
}

well...

... one thing that bothers me with the ADUserCache class as it currently stands, is that we've spent time implementing this fine piece of abstraction for performance gains in all its glory, but next week we'll be writing a script that requires almost the same thing, but consuming some resource other than Active Directory - a web page, a RESTful API, a database, a remote file system - who knows?!

Wouldn't it be nice if we could reuse our glorious cache class without copy-pasting and modifying the ADUserCache definition?

Stay tuned for more class-based caching goodness...