Extending the ADUserCache class

The ADUserCache class that we wrote previously seems to be a pretty good base to start from, let's see what else we can do with it.

Extending our cache class

Another useful behavior we might want to implement could be the ability for pre-seed the cache. I mean, we already have the $Agents list of users in memory, and populating a hash table is not terribly expensive (and we only need to do it once!).

As previously mentioned, PowerShell classes allow us to overload methods and constructors just like in C#. That is, provide multiple implementations of the same method with different signatures - the distinct combination of return type, parameter types and method name.

Overloading constructors!

Let's overload the constructor:

class ADUserCache  
  # ...
  ADUserCache([psobject[]]$InitialUsers){
    $this.LookupTable = @{}
    foreach($User in $InitialUsers){
      $this.LookupTable[$User.DistinguishedName] = $User
    }
  }
  # ...
}

Awesome, now we can re-use the set of user account objects that we've already collected:

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

# Start out with a pre-seeded lookup table
$UserCache = [ADUserCache]::new($Agents)

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

Another feature that might come in handy is the ability to clear our cache, and maybe selectively evict individual items.

Let's add those as well:

Clear()

The [hashtable] type already has a method for this: Clear() - we'll reuse the method signature (more about why later), and implement a simple wrapper method in our ADUserCache class:

class ADUserCache  
  # ...
  [void] Clear(){
      $this.LookupTable.Clear()
  }
  # ...
}

Remove()

Next up is the ability to clear individual items from our cache. Once again, [hashtable] already exposes this functionality with the Remove() method - once again we just wrap it in a method of the same name, this time passing a string argument to identify the relevant item in the cache:

class ADUserCache  
  # ...
  [void] Remove([string]$Identity){
      $this.LookupTable.Remove($Identity)
  }
  # ...
}

With this in place, we can allow the user to clear just a single entry without starting completely over from scratch.

Cache operator's unintended call to Clear()

Refresh()

Lastly, we might want a "refresh" method - a method that acts like GetADUser(), but forces a refresh of the cache entry for that item regardless of whether it's already present or not.

This time we'll have to come up with the logic ourselves, as [hashtable] doesn't already have something like this (for the good reason that it's just a hashtable - not a specialized cache type).

We can reuse the functions we already have though, so no worries:

class ADUserCache  
  # ...
  [psobject] Refresh([string]$Identity){
      $this.Remove($Identity)
      return $this.GetADUser($Identity)
  }
  # ...
}

Another common pattern one might find in a cache implementation like ours is an optional boolean that forces the cache to go out and fetch a fresh copy of our data, like so:

class ADUserCache  
  # ...
  [psobject] GetADUser([string]$Identity, [bool]$ForceRefresh){
    if((-not $ForceRefresh) -and $this.LookupTable.Contains($Identity)){
      return $this.LookupTable[$Identity]
    }
    else{
      return ($this.LookupTable[$Identity] = Get-ADUser -Identity $Identity -ErrorAction SilentlyContinue) 
    }
  }  # ...
}

You'll notice that this code is almost identical to the original implementation of GetADUser() - so let's re-write its call chain t reuse this new overload (did I mention that I hate repeating code?):

class ADUserCache  
  # ...
  [psobject] GetADUser([string]$Identity){
      return $this.GetADUser($Identity, $false)
  }

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

I personally don't like this pattern, so I'll be sticking with a separate Refresh() method.

This is obviously highly subjective, so feel free to pursue whatever strategy works for you!

Final implementation

class ADUserCache  
{
  [hashtable]$LookupTable

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

  ADUserCache([psobject[]]$InitialUsers){
    $this.LookupTable = @{}
    foreach($User in $InitialUsers){
      $this.LookupTable[$User.DistinguishedName] = $User
    }
  }

  [void] Clear(){
      $this.LookupTable.Clear()
  }

  [void] Remove([string]$Identity){
      $this.LookupTable.Remove($Identity)
  }

  [psobject] Refresh([string]$Identity){
      $this.Remove($Identity)
      return $this.GetADUser($Identity)
  }

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

Conclusion

The ADUserCache class is starting to look, act and feel like a real caching facility, how about that!

However, as I mentioned in the last post in this series, I have a huge pet peeve with the direction I've taken here:

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

Next up, we'll look into generalizing our cache implementation - stay tuned!