2

I'm attempting to utilize PowerShell's inheritance to create a class that inherits from an existing class and I am not seeing the expected behavior based on various sources.

Here's a brief example highlighting the problem:

class child : System.Xml.XmlDocument {
    [String] ToString(){
        return 'ChildString'
    }

    [void] LoadXml([string]$content) {
        Write-Host 'LoadXml'
        ([System.Xml.XmlDocument]$this).LoadXml($content)
    }
}

If I run

Write-Host 'XMLDocument Object'
$xml=New-Object xml
$xml.ToString()

Write-Host 'Inherited Object'
$child=New-Object child
$child.ToString()

The console output is

XMLDocument Object
#document
Inherited Object
#document

indicating that the ToString() method of the child class is NOT being utilized.

On the opposite end of the spectrum, if I call $child.LoadXml('<a>Hello</a>') then I get infinite recursion, implying that the call to the parent object is failing in that method.

Based on what I've been able to find I would not expect these methods to behave this way and I was hoping that someone could help me understand what I am missing. I am generally more used to Python; however, for security reasons I can't implement this code in another programming language (I can't install software on the computer this is to be run on).

Thanks!


Here's the results of $PSversionTable.PSVersion for reference:

Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      15063  909    
Matt
  • 33
  • 4

2 Answers2

3

In both cases, the call to ToString() is overridden by additional type data attached to their common ancestor System.Xml.XmlNode:

PS C:\> Get-TypeData -TypeName System.Xml.XmlNode

TypeName           Members                                                            
--------           -------                                                            
System.Xml.XmlNode {[ToString, System.Management.Automation.Runspaces.CodeMethodData]}

You can use Remove-TypeData to remove it at runtime:

Remove-TypeData -TypeName System.Xml.XmlNode

You should now see your child object invoke its own method and the XmlDocument object invoke ToString() as inherited from System.Object:

XMLDocument Object
System.Xml.XmlDocument
Inherited Object
ChildString
mklement0
  • 382,024
  • 64
  • 607
  • 775
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • That's really interesting and solved 99% of my issues. I will definitely have to look into TypeData a bit more. Thanks! – Matt Mar 13 '18 at 16:00
  • 1
    @mklement0 Thanks! Although annoying, I believe this member resolution order is essential to the behavior of ETS. The only bug I see in the original question is the `LoadXml` thing. – Mathias R. Jessen Mar 13 '18 at 19:05
  • 1
    I posted an issue re `LoadXml` calling itself instead of the base class: https://github.com/PowerShell/PowerShell/issues/6386 – mklement0 Mar 13 '18 at 22:04
1

Mathias R. Jessen's great answer explains and resolves the problem with the shadowed .ToString() method.
Even though the behavior may be obscure, it appears to be by design: PowerShell's ETS (Extended Type System), which allows customization of .NET types, gives precedence to such customizations even in derived classes.

By contrast, the inability to call the base class method from your .LoadXml() method smells like a bug, reported in this GitHub issue [fixed as of at least PowerShell Core v6.1.0], and it sounds like the bug is specific to classes that derive from System.Xml.XmlNode.

Here's a workaround, gratefully adapted from this answer:

class child : System.Xml.XmlDocument {
  [void] LoadXml([string]$content) {
    Write-Host 'LoadXml'
    # Get a function pointer to the base class method...
    $funcPtr = [System.Xml.XmlDocument].GetMethod('LoadXml', [string]).MethodHandle.GetFunctionPointer() 
    # ...and invoke it with this instance.
    [Activator]::CreateInstance([Action[string]], $this, $funcPtr).Invoke($content)
  }
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    For my purposes, I can get around utilizing the LoadXml function, it was just quite surprising when I tested it that it behaved that way. Combined with the other behavior, I thought that I was going insane. Anyway, I tested this workaround and it does solve the problem. I wish I could mark both answers as accepted. Thanks! – Matt Mar 14 '18 at 14:01