iRon
iRon

Reputation: 23623

How to extend a dictionary class

I would like to create a custom dictionary class in PowerShell (which temporary stores a configuration in a specific 3rd party application and retrieves it in another PowerShell session). Technically, I could also do this by creating a kind of Set-MyConfigVariable -Key <key> -Value <Value> and Get-MyConfigVariable -Key <key> cmdlets, but I would like to investigate if I can make in more user (author) frindly with a extended dictionary (and hook in the getterand setter methods).
In short, something that is discribed here in C#: How to write a getter and setter for a Dictionary?

Class MyStore: System.Collections.Generic.Dictionary[String,String] {
    $h = @{}
    [string]$This([String]$Key) {
        get { write-host 'getting something'; return $h[$Key] }
        set { write-host 'setting something'; $h[$Key] = $Value }
    }
}

$MyStore = [MyStore]::new()
$MyStore['key'] = 'value'
$MyStore['key']

The above prototype fails at [string]$This([String]$Key) { (the indexer?) but probably also need a different implementation for the getter and setter (using Update-TypeData?).

How can this be done in PowerShell (if even possible)?
If not possible for a class, can it be done on an single instance (with dynamic keys)?

Upvotes: 2

Views: 316

Answers (1)

mklement0
mklement0

Reputation: 437100

PowerShell code, using custom classes, has the following limitations as of PowerShell 7.3.4:

  • There's generally no support for property getters and setters - GitHub issue #2219 proposes adding support for them.

  • For now, getters and setters for regular properties can be emulated, using Add-Member with a ScriptProperty member, as shown in this answer.Tip of the hat to Santiago Squarzon.

  • However, there is no workaround for implementing parameterized properties, as is required for implementing indexers.

C# code, ad hoc-compiled with Add-Type, offers a solution:

  • Deriving your custom class from Dictionary[String,String] - as you've attempted - is problematic, because it requires hiding the base class members with the new keyword, which in PowerShell code - at least as of PowerShell 7.3.4 - isn't fully supported - see GitHub issue #19649.

  • However, you can make your custom class wrap a dictionary type, and provide access to it via a type-specific indexer (parameterized property), into which you can hook PowerShell script blocks to execute.

See the following sample implementation:

Add-Type @'
using System;
using System.Collections.Generic;
using System.Management.Automation;
public class CustomDict
{
  // The dictionary to wrap.
  Dictionary<string, string> _dict = new ();
  // The hook script blocks.
  ScriptBlock _getHook, _setHook;
  // Constructor
  public CustomDict(ScriptBlock getHook = null, ScriptBlock setHook = null)
  {
    _getHook = getHook; _setHook = setHook;
  }
  // Implement an indexer that provides access to that of the wrapped 
  // dictionary, while also executing the "event"-hook script blocks.
  public string this[string key]
  {
      get { 
        _getHook?.Invoke(_dict, key);
        return _dict[key]; 
      }
      set { 
        _setHook?.Invoke(_dict, key, value);
        _dict[key] = value;
      }
  }
}
'@

# Construct an instance with both GET and SET hooks (script blocks).
$o = [CustomDict]::new(
  { param($dict, $key) Write-Verbose -Verbose "getting entry '$key'" }, 
  { param($dict, $key, $value )Write-Verbose -Verbose "setting '$key' to '$value'" }
)

# Create an entry...
$o['foo'] = 'bar'
# ... and report its value
$o['foo']

Output:

VERBOSE: setting 'foo' to 'bar'
VERBOSE: getting entry 'foo'
bar

Note:

  • The script blocks passed as hooks execute in a child scope of the caller.

Upvotes: 2

Related Questions