Public/Add-PrivateMember.ps1

using namespace System.Reflection
using namespace System.Linq
using namespace System.Management.Automation

function Add-PrivateMember {
    <#
    .EXTERNALHELP ImpliedReflection-help.xml
    #>

    [CmdletBinding(PositionalBinding=$false)]
    param(
        [Parameter(Position=0)]
        [string]
        $ReturnPropertyName,

        [Parameter(ValueFromPipeline)]
        [psobject]
        $InputObject,

        [switch]
        $PassThru
    )
    begin {
        $FLAG_MAP = @{
            StaticAll = [BindingFlags]'Static, NonPublic, Public'
            Static    = [BindingFlags]'Static, NonPublic'
            Instance  = [BindingFlags]'Instance, NonPublic'
        }

        function GetCacheEntry([MemberInfo[]] $member) {
            if ($member[0] -is [MethodBase]) {
                return NewMethodCacheEntry $member
            }
            return NewPropertyCacheEntry $member[0]
        }

        function NewPropertyCacheEntry([MemberInfo] $property) {
            $cacheEntry = [ref].Assembly.
                GetType('System.Management.Automation.DotNetAdapter+PropertyCacheEntry').
                GetConstructor($FLAG_MAP.Instance, $null, @($property.GetType()), 1).
                Invoke($property)

            # The cache entry uses these fields to determine if the properties for the
            # Getter/Setter delegates should create the delegate. They are set to false during
            # construction if the method isn't public.
            $cacheEntry.GetType().GetField($script:FIELD_REFERENCE.writeOnly, 60).SetValue($cacheEntry, $false)
            $isWritable = $property.SetMethod -or
                         ('Field' -eq $property.MemberType -and
                          -not $property.IsInitOnly)

            if ($isWritable) {
                $cacheEntry.GetType().GetField($script:FIELD_REFERENCE.readOnly, 60).SetValue($cacheEntry, $false)
            }
            return $cacheEntry
        }

        function NewMethodCacheEntry([MethodBase[]] $methods) {
            # PowerShell tries to flatten the arrays without explicitly constructing them like this.
            $argumentList = [object[]]::new(1)
            $argumentList[0] = $methods

            return [ref].
                Assembly.
                GetType('System.Management.Automation.DotNetAdapter+MethodCacheEntry').
                GetConstructor($FLAG_MAP.Instance, $null, @([MethodBase[]]), 1).
                Invoke($argumentList)
        }

        function NewPSMethod([Lookup[string, MethodInfo]] $methodGroups) {
            foreach ($methodGroup in $methodGroups) {
                if ($InputObject.psobject.Methods[$methodGroup.Key]) { continue }
                $adapterData = NewMethodCacheEntry $methodGroup

                # Mainly cosmetic, just shows or doesn't show in Get-Member if this is set.
                $isSpecial = $methodGroup.Key -match '(set|get|add|remove)_\w+$'

                $psMethod = [PSMethod].InvokeMember(
                    <# name: #> '',
                    <# invokeAttr: #> [BindingFlags]'CreateInstance, Instance, NonPublic',
                    <# binder: #> $null,
                    <# target: #> $null,
                    <# args: #> @(
                        <# name: #> $methodGroup.Key,
                        <# adapter: #> $bindings.Adapter,
                        <# baseObject: #> $InputObject.psobject.BaseObject,
                        <# adapterData: #> $adapterData,
                        <# isSpecial: #> $isSpecial,
                        <# isHidden: #> $isSpecial))

                # The true here is "preValidated", it throws an exception about not being able to add
                # PSMethods if you don't claim to have validated it.
                $psMethod # yield
            }
        }

        function BindInstanceProperties([MemberInfo[]] $properties) {
            foreach ($property in $properties) {

                if ($InputObject.psobject.Properties[$property.Name]) { continue }

                $adapterData = NewPropertyCacheEntry $property

                $psProperty = [PSProperty].InvokeMember(
                    <# name: #> '',
                    <# invokeAttr: #> [BindingFlags]'CreateInstance, Instance, NonPublic',
                    <# binder: #> $null,
                    <# target: #> $null,
                    <# args: #> @(
                        <# name: #> $property.Name,
                        <# adapter: #> $bindings.Adapter,
                        <# baseObject: #> $InputObject.psobject.BaseObject,
                        <# adapterData: #> $adapterData))

                $InputObject.psobject.Properties.Add($psProperty, $true)
            }
        }

        function GetCacheTable([string] $memberType) {
            return $bindings.Adapter.
                GetType().
                GetMethod("GetStatic${memberType}ReflectionTable", $FLAG_MAP.Static).
                Invoke($null, @($bindings.Type))
        }

        function GetBindingInfo([psobject] $target) {
            if ($target -is [type]) {
                return @{
                    IsStatic = $true
                    Type     = $target.psobject.BaseObject
                    Flags    = $FLAG_MAP.Static
                    Adapter  = [psobject].
                        GetField($script:FIELD_REFERENCE.dotNetStaticAdapter, $FLAG_MAP.Static).
                        GetValue($null)
                }
            }
            return @{
                IsStatic = $false
                Type     = $target.GetType()
                Flags    = $FLAG_MAP.Instance
                Adapter  = [PSMethod].
                    GetField($script:FIELD_REFERENCE.adapter, $FLAG_MAP.Instance).
                    GetValue($target.psobject.Methods.Item('GetType'))
            }
        }
    }
    process {
        $bindings   = GetBindingInfo $InputObject
        $members = @{
            Method    = $bindings.Type.GetMethods($bindings.Flags)
            Property  = $bindings.Type.GetProperties($bindings.Flags) +
                        $bindings.Type.GetFields($bindings.Flags)
        }

        # Group methods by name so we can add them to the same cache entry.
        if ($members.Method) {
            $members.Method = [Enumerable]::ToLookup(
                $members.Method,
                [Func[MethodInfo, string]]{ $args[0].Name })
        }

        if ($bindings.IsStatic) {
            # Cache the add method for the loop.
            $add = [ref].
                Assembly.
                GetType('System.Management.Automation.CacheTable').
                GetMethod('Add', $FLAG_MAP.Instance)

            foreach ($memberType in 'Method', 'Property') {
                $table = GetCacheTable $memberType
                foreach ($member in $members.$memberType) {

                    $memberName = $member.Key
                    if (-not $memberName) { $memberName = $member.Name }

                    $alreadyExists = $table.
                        GetType().
                        GetField($script:FIELD_REFERENCE.indexes, $FLAG_MAP.Instance).
                        GetValue($table).
                        ContainsKey($memberName)

                    if (-not $alreadyExists) {
                        $cacheEntry = GetCacheEntry $member

                        $null = $add.Invoke($table, @($memberName, $cacheEntry))

                    }
                }
            }
        } else {
            $psMethods = NewPSMethod $members.Method
            foreach ($psMethod in $psMethods) {
                $InputObject.psobject.Methods.Add($psMethod, $true)
            }
            BindInstanceProperties $members.Property
        }
        if ($ReturnPropertyName) {
            $InputObject.$ReturnPropertyName
        } elseif ($PassThru.IsPresent) {
            $InputObject # yield
        }
    }
}