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:
Senior software developers trying to teach IT professionals how to write code. pic.twitter.com/CpUSUBu9Qg
— Adam Bertram (@adbertram) September 25, 2017
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...