Home > OS >  How to extend and override Add in a collection class
How to extend and override Add in a collection class

Time:01-01

Background

I have a data object in PowerShell with 4 properties, 3 of which are strings and the 4th a hashtable. I would like to arrange for a new type that is defined as a collection of this data object.

In this collection class, I wish to enforce a particular format that will make my code elsewhere in the module more convenient. Namely, I wish to override the add method with a new definition, such that unique combinations of the 3 string properties add the 4th property as a hashtable, while duplicates of the 3 string properties simply update the hashtable property of the already existing row with the new input hashtable.

This will allow me to abstract the expansion of the collection and ensure that when the Add method is called on it, it will retain my required format of hashtables grouped by unique combinations of the 3 string properties.

My idea was to create a class that extends a collection, and then override the add method.

Code so far

As a short description for my code below, there are 3 classes:

  1. A data class for a namespace based on 3 string properties (which I can reuse in my script for other things).
  2. A class specifically for adding an id property to this data class. This id is the key in a hashtable with values that are configuration parameters in the namespace of my object.
  3. A 3rd class to handle a collection of these objects, where I can define the add method. This is where I am having my issue.
Using namespace System.Collections.Generic

Class Model_Namespace {
    [string]$Unit
    [string]$Date
    [string]$Name

    Model_Namespace([string]$unit, [string]$date, [string]$name) {
        $this.Unit = $unit
        $this.Date = $date
        $this.Name = $name
    }
}

Class Model_Config {
    [Model_Namespace]$namespace
    [Hashtable]$id
    
    Model_Config([Model_Namespace]$namespace, [hashtable]$config) {
        $this.namespace = $namespace
        $this.id = $config
    }
    Model_Config([string]$unit, [string]$date, [string]$name, [hashtable]$config) {
        $this.namespace = [Model_Namespace]::new($unit, $date, $name)
        $this.id = $config
    }
}

Class Collection_Configs {
    $List = [List[Model_Config]]@()

    [void] Add ([Model_Config]$newConfig ){
        $checkNamespaceExists = $null

        $u  = $newConfig.Unit
        $d  = $newConfig.Date
        $n  = $newConfig.Name
        $id = $newConfig.id

        $checkNamespaceExists = $this.List | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }

        If ($checkNamespaceExists){
            ($this.List | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }).id  = $id
        }
        Else {
            $this.List.add($newConfig)
        }
    }
}

Problem

I would like the class Collection_Configs to extend a built-in collection type and override the Add method. Like a generic List<> type, I could simply output the variable referencing my collection and automatically return the collection. This way, I won't need to dot into the List property to access the collection. In fact I wouldn't need the List property at all.

However, when I inherit from System.Array, I need to supply a fixed array size in the constructor. I'd like to avoid this, as my collection should be mutable. I tried inheriting from List, but I can't get the syntax to work; PowerShell throws a type not found error.

Is there a way to accomplish this?

CodePudding user response:

It is possible to subclass System.Collections.Generic.List`1, as this simplified example, which derives from a list with [regex] elements, demonstrates:[1]

using namespace System.Collections.Generic

# Subclass System.Collections.Generic.List`1 with [regex] elements.
class Collection_Configs : List[regex] {

    # Override the .Add() method.
    # Note: You probably want to override .AddRange() too.
    Add([regex] $item) {
      Write-Verbose -Verbose 'Doing custom things...'
      # Call the base-class method.
      ([List[regex]] $this).Add($item)
    }

  }

# Sample use.
$list = [Collection_Configs]::new()
$list.Add([regex] 'foo')
$list

However, there's an additional challenge in your case:

  • As of PowerShell 7.3.1, because the generic type argument that determines the list element type is another custom class, that other class must unexpectedly be loaded beforehand, in a separate script, the script that defines the dependent Collection_Configs class.

    • This requirement is unfortunate, and at least conceptually related to the general (equally unfortunate) need to ensure that .NET types referenced in class definitions have been loaded before the enclosing script executes - see this post, whose accepted answer demonstrates workarounds.

    • However, given that all classes involved are part of the same script file in your case, a potential fix should be simpler than the one discussed in the linked post - see GitHub issue #18872.


[1] Note: There appears to be a bug in Windows PowerShell, where calling the base class' .Add() method fails if the generic type argument (element type) happens to be [pscustomobject] aka [psobject]: That is, while ([List[pscustomobject]] $this).Add($item) works as expected in PowerShell (Core) 7 , an error occurs in Windows PowerShell, which requires the following reflection-based workaround: [List[pscustomobject]].GetMethod('Add').Invoke($this, [object[]] $item)

CodePudding user response:

There were a few issues with the original code:

The Using keyword was spelled incorrectly. It should be using. The $List variable in the Collection_Configs class was not declared with a type. It should be [List[Model_Config]]$List. The Add method in the Collection_Configs class was missing its return type. It should be [void] Add ([Model_Config]$newConfig). The Add method was missing its opening curly brace.

  • Related