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!'
class
keyword (and support for classes) was introduced with PowerShell 5.0 back in 2016.New-Object
cmdlet has since fallen out of preferential favor with newer versions of PowerShell -- and continues to see little use today, with the exception of implementing backwards compatability (e.g. PowerShell 4.0) and authoring scripts/functions/modules that support multiple OS/PowerShell generations.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:
Constructors: If the destination type has a constructor taking a single argument whose type is that of the source type, that constructor is called to perform the conversion.
class Foo {
[string] $FooProp
Foo([string] $foo) {
$this.FooProp = $foo
}
}
[Foo] 'OK!'
Parse Method: If the source type is string and the destination type has a method called Parse
, that method is called to perform the conversion.
class Foo {
[string] $FooProp
static [Foo] Parse([string] $foo) {
return [Foo]@{ FooProp = $foo }
}
}
[Foo] 'OK!'
Explicit Cast Operator: If the source type has an explicit cast operator that converts to the destination type, that operator is called to perform the conversion. If the destination type has an explicit cast operator that converts from the source type, that operator is called to perform the conversion.
class Foo {
[string] $FooProp
static [Foo] op_Explicit([string] $foo) {
return [Foo]@{ FooProp = $foo }
}
}
[Foo] 'OK!'
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'))
PSTypeAdapter
as workaroundThis 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.
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)
}
}
Update-TypeData
using this new type as the -TypeConverter
parameter:$updateTypeDataSplat = @{
TypeName = [System.IO.StreamReader]
TypeConverter = [StreamReaderConverter]
}
Update-TypeData @updateTypeDataSplat
$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;
}
}
}