Before we get into the how, here is why you should care about this! Consider the following oversimplified function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
Function new-Server { <# .SYNOPSIS Create a new GetAdmin.Server object .PARAMETER Name Server Name .PARAMETER Network Detailed per interfave network information .EXAMPLE Get-NavFiler .Outputs Netapp.SDK.NavFiler[] #> [CmdletBinding()] param( [Parameter(ValueFromPipelineByPropertyName=$TRUE)] [string] $Name, [Parameter()] [GetAdmin.Net[]] $Network ) Process { return New-Object GetAdmin.Server -ArgumentList @($Name, $network) } } |
First off we need to load our custom types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
Add-Type -Language CSharpVersion3 -TypeDefinition @” using System.Collections; namespace GetAdmin { public class Net { #region Paramaters public string Interface { get; set; } public System.Net.IPAddress IPAddress { get; set; } public string Netmask { get; set; } #endregion Parameters #region Constructors public Net() { Interface = null; IPAddress = new System.Net.IPAddress((long)16777343); Netmask = null; } public Net( string name, System.Net.IPAddress ipaddress, string netmask ) { Interface = name; IPAddress = ipaddress; Netmask = netmask; } #endregion Constructors } public class Server { #region Paramaters public string Name { get; set; } public ArrayList Network { get; set; } #endregion Parameters #region Constructors public Server() { Name = null; Network = new ArrayList(); } public Server( string name, ArrayList network ) { Name = name; Network = network; } #endregion Constructors } } “@ |
Now let try and use this function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[0:3]PS> $Network = 1..3 | Foreach { New-Object GetAdmin.Net("ns0", "192.168.1.$_", "255.255.255.0") } [0:4]PS> $network Interface IPAddress Netmask --------- --------- ------- ns0 192.168.1.1 255.255.255.0 ns0 192.168.1.2 255.255.255.0 ns0 192.168.1.3 255.255.255.0 [0:5]PS> New-Server -Name Server1 -Network $Network Name Network ---- ------- Server1 {192.168.1.1, 192.168.1.2, 192.168.1.3} |
While that’s not that bad, consider the following, it could look like this.
1 2 3 4 5 |
[0:4]PS> New-Server –Name Server1 –Network "ns0,192.168.1.1,255.255.255.0", "ns0,192.168.1.2,255.255.255.0", "ns0,192.168.1.3,255.255.255.0" Name Network ---- ------- Server1 {192.168.1.1, 192.168.1.2, 192.168.1.3} |
Now I really don’t want to get into which one you should be using. I prefer instead to focus on how to enable either one!
First things first we’re going to have to get a little dirty with some C# (Again I’m not a developer so if anyone knows a better way to do this I’m all ears.) and create a type converter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
Add-Type -Language CSharpVersion3 -TypeDefinition @” using System; using System.Collections; using System.Management.Automation; namespace GetAdmin { public class NetConverter : PSTypeConverter { /// Override for the CanConvertFrom Method. /// Returns true if the Source object /// is of type String and can be Converted to GetAdmin.Net type public override bool CanConvertFrom(Object sourceValue, Type destinationType) { string src = sourceValue as string; if (src != null) { try { string[] Fields = src.Split(new char[1] { ',' }); if (Fields.GetLength(0) == 3) { return true; } } catch (Exception) { return false; } } return false; } /// Override for the ConvertFrom Method public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider provider, bool IgnoreCase) { if (sourceValue == null) throw new InvalidCastException("no conversion possible"); if (this.CanConvertFrom(sourceValue, destinationType)) { try { // Cast our input as a string just in case string src = sourceValue as string; // create a new GetAdmin.Net object GetAdmin.Net n = new GetAdmin.Net(); if (src != null || src != "") { //split our string into an array using commas as the delim string[] Fields = src.Split(new char[1] { ',' }); //populate our new object n.Interface = Fields[0]; n.IPAddress = System.Net.IPAddress.Parse(Fields[1]); n.Netmask = Fields[2]; } //return our new GetAdmin.Net Object return n; } catch (Exception) { throw new InvalidCastException("no conversion possible"); } } throw new InvalidCastException("no conversion possible"); } /// Default to PowerShell conversion for other types. /// Return False here public override bool CanConvertTo(object Value, Type destinationType) { return false; } /// Do not handle conversion for other types public override object ConvertTo(object Value, Type destinationType, IFormatProvider provider, bool IgnoreCase) { throw new InvalidCastException("conversion failed"); } } public class Net { #region Paramaters public string Interface { get; set; } public System.Net.IPAddress IPAddress { get; set; } public string Netmask { get; set; } #endregion Parameters #region Constructors public Net() { Interface = null; IPAddress = new System.Net.IPAddress((long)16777343); Netmask = null; } public Net( string name, System.Net.IPAddress ipaddress, string netmask ) { Interface = name; IPAddress = ipaddress; Netmask = netmask; } #endregion Constructors } public class Server { #region Paramaters public string Name { get; set; } public ArrayList Network { get; set; } #endregion Parameters #region Constructors public Server() { Name = null; Network = new ArrayList(); } public Server( string name, ArrayList network ) { Name = name; Network = network; } #endregion Constructors } } “@ |
I told you we needed to get a little dirty! Now that we’ve implemented a type converter that knows how to convert a string to a GetAdmin.Net object. Now we need to inform PowerShell of our work. This is accomplished via the a type formatting file.
1 2 3 4 5 6 7 8 |
<Types> <Type> <Name>GetAdmin.Net</Name> <TypeConverter> <TypeName>GetAdmin.NetConverter</TypeName> </TypeConverter> </Type> </Types> |
Next import your new type definition file, and try it out!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[0:1]PS> Update-FormatData DocumentsGetAdmin.Format.ps1xml [0:2]PS> Update-TypeData DocumentsGetAdmin.Types.ps1xml [0:3]PS> [GetAdmin.Net[]]("eth0,192.168.1.1,255.255.255.0","eth1,192.168.2.2,255.255.255.0") Interface IPAddress Netmask --------- --------- ------- eth0 192.168.1.1 255.255.255.0 eth1 192.168.2.2 255.255.255.0 [0:4]PS> New-Server –Name Server1 –Network "ns0,192.168.1.1,255.255.255.0", "ns0,192.168.1.2,255.255.255.0", "ns0,192.168.1.3,255.255.255.0" Name Network ---- ------- Server1 {192.168.1.1, 192.168.1.2, 192.168.1.3} |
Hope that was helpful, know a better way?
~Glenn
Great post!
One question, why don’t you treat the netmask as a System.Net.IPAddress as well ?
Would give you validation and avoid a netmask like “345.768.999.876”
Thank you,
I modified some code from PoshOnTap for the purpose of this post, and well… I never thought about it. I looked for a .net type that was netmask, but couldn’t find one. As I think about it there isn’t any reason I couldn’t have casted netmask as an IPAddress as well! In my source code I used a regex to validate the input on netmask, but I removed it for simplicity. I am going to have to play with it some more b/c that could be a very elegant solution.
As a side the real advantage to casting to IPAddress is the flexibility. [System.Net.IPAddress] is aware of both IPv4 and IPv6! IPv6 being the real pain as the shorthand makes “manual” validation a real pain.
Thanks for article. AFAIK there is another method to convert parameters – using [Transform()] in parameter section of function. In this case you can write only POSH code without need to C#’ing. But it brings some issues with binding piped parameters.
This post gave me the pointers I needed to get started. Thank you for that.
With powershell 3.0 or later, there are new features that make it easier.
The converter class can be written as a powershell class. It does not need to be C#. See the answer on Stack here:
https://stackoverflow.com/questions/65112153/how-to-provide-custom-type-conversion-of-a-powershell-parameter/65113127#65113127
You can use Update-TypeData with the -TypeConverter parameter without having to set up a .ps1xml file. It can be as simple as update-typedata -TypeName “sourceType” -TypeConverter “ClassName” -force.