.netpowershellconstructortype-conversionpowershell-4.0

Constructor invocation syntax in PowerShell 4.0 and lower without using New-Object


I ran across some older code today that piqued my curiosity. I'm aware of 2x different syntactical ways of instantiating .NET objects in PowerShell 4.0

# Using New-Object to pass multiple args to the constructor
$ex = New-Object -TypeName System.IO.FileNotFoundException -ArgumentList 'No File Found!', 'C:\NoFile.txt'


# Alternate Syntax - how can you pass multiple args?
$ex = [System.IO.FileNotFoundException] 'No File Found!'



[EDIT] -- NOTES MOTIVATED BY COMMENTS



Questions:

  1. Where is this alternate syntax documented? I haven't yet found anything online in terms of official documentation.
  2. How can you pass multiple arguments using this alternate syntax and call a specific constructor overload?
  3. Is there a name for this alternate syntax?

Solution

  • Where is this alternate syntax documented?

    In 6.18 .NET Conversion, you can create an object by casting when one of the conversion rules are met. A few of them, the most commonly known:

    How can you pass multiple arguments using this alternate syntax and call a specific constructor overload?

    You cannot as-is, you can work your way around it using a PSTypeConverter, see bottom section of this answer for details.

    The closer could be in 5.1 when they would allow you to instantiate using a hashtable literal:

    class Foo {
        [string] $FooProp
        [string] $BarProp
    }
    
    [Foo]@{ FooProp = 'OK!'; BarProp = '123' }
    

    And, this one added in 5.0, the ::new intrinsic member:

    class Foo {
        [string] $FooProp
        [string] $BarProp
    
        Foo([string] $foo, [string] $bar) {
            $this.FooProp, $this.BarProp = $foo, $bar
        }
    }
    
    [Foo]::new('OK!', '123')
    

    Or perhaps reflection (not even close to what you're looking for, also not even sure if this works in 4.0).

    [Foo].GetConstructor(@([string], [string])).Invoke(@('OK!', '123'))
    

    Using a PSTypeAdapter as workaround

    This simplified example demos how you can use a PSTypeAdapter to cast from object[] to StreamReader targeting the StreamReader(String, Encoding) overload. Source value type could be different from object[], for example you could use cast from hash table.

    In this case, I'm not sure if this could work and how you could make this work in PowerShell 4.0, PowerShell classes where only made available in PowerShell 5.1. Perhaps a compiled class could work.

    1. First define a class that inherits from PSTypeAdapter:
    class StreamReaderConverter : System.Management.Automation.PSTypeConverter
    {
        [bool] CanConvertFrom([Object] $sourceValue, [type] $destinationType)
        {
            if ($destinationType -ne [System.IO.StreamReader]) {
                return $false
            }
    
            if ($sourceValue -isnot [array] -or $sourceValue.Count -ne 2) {
                return $false
            }
    
            $path, $enc = $sourceValue
            if ($enc -isnot [System.Text.Encoding] -or $path -isnot [string]) {
                return $false
            }
    
            return $true
        }
    
        [Object] ConvertFrom(
            [Object] $sourceValue,
            [type] $destinationType,
            [System.IFormatProvider] $formatProvider,
            [bool] $ignoreCase)
        {
            $file = Get-Item $sourceValue[0] -ErrorAction Ignore
            if (-not $file -or $file -isnot [System.IO.FileInfo])
            {
                throw [System.Management.Automation.ItemNotFoundException]::new(
                    "Cannot find path '$($sourceValue[0])' because it does not exist or is not a file.")
            }
    
            # targets `public StreamReader(string path, Encoding encoding);` overload
            return [System.IO.StreamReader]::new(
                $file.FullName,
                [System.Text.Encoding] $sourceValue[1])
        }
    
        [bool] CanConvertTo([Object] $sourceValue, [type] $destinationType)
        {
            return $this.CanConvertFrom($sourceValue, $destinationType)
        }
    
        [Object] ConvertTo(
            [Object] $sourceValue,
            [type] $destinationType,
            [System.IFormatProvider] $formatProvider,
            [bool] $ignoreCase)
        {
            return $this.ConvertFrom(
                $sourceValue, $destinationType, $formatProvider, $ignoreCase)
        }
    }
    
    1. Then Update-TypeData using this new type as the -TypeConverter parameter:
    $updateTypeDataSplat = @{
        TypeName      = [System.IO.StreamReader]
        TypeConverter = [StreamReaderConverter]
    }
    Update-TypeData @updateTypeDataSplat
    
    1. Lastly, test:
    $reader = [System.IO.StreamReader] ('.\myFile.txt', [System.Text.Encoding]::ASCII)
    $reader.CurrentEncoding # should be ASCII
    

    In PowerShell 4 you might be able to define your own type via Add-Type I'm not sure what C# version it supports though, the following should work fine in PowerShell 5.1.

    You can Add-Type -TypeDefinition code below and then Update-TypeData as demoed above.

    using System;
    using System.IO;
    using System.Management.Automation;
    using System.Text;
    
    public sealed class StreamReaderConverter : PSTypeConverter
    {
        public override bool CanConvertFrom(object sourceValue, Type destinationType)
        {
            if (destinationType != typeof(StreamReader))
            {
                return false;
            }
    
            object[] args = sourceValue as object[];
            if (args == null || args.Length != 2)
            {
                return false;
            }
    
            SetBaseObjectIfPSobject(ref args[0]);
            SetBaseObjectIfPSobject(ref args[1]);
    
            if (args[0] is string && args[1] is Encoding)
            {
                return true;
            }
    
            return false;
        }
    
        public override bool CanConvertTo(object sourceValue, Type destinationType)
        {
            return CanConvertFrom(sourceValue, destinationType);
        }
    
        public override object ConvertFrom(
            object sourceValue,
            Type destinationType,
            IFormatProvider formatProvider,
            bool ignoreCase)
        {
            object[] args = (object[])sourceValue;
            string path = Path.GetFullPath((string)args[0]);
    
            if (!File.Exists(path))
            {
                throw new ItemNotFoundException(string.Format(
                    "Cannot find path '{0}' because it does not exist or is not a file.", path));
            }
    
            return new StreamReader(path, (Encoding) args[1]);
        }
    
        public override object ConvertTo(
            object sourceValue,
            Type destinationType,
            IFormatProvider formatProvider,
            bool ignoreCase)
        {
            return ConvertFrom(
                sourceValue, destinationType, formatProvider, ignoreCase);
        }
    
        private static void SetBaseObjectIfPSobject(ref object value)
        {
            if (value is PSObject)
            {
                value = ((PSObject)value).BaseObject;
            }
        }
    }