How to dynamically add new properties to custom object in PowerShell - powershell

I have a custom object that contains device information that looks something like this.
name,model,sn
PC1,Elitebook 850, ABC123,
PC2,EliteDesk 600,123ABC
I have a function that retrieves threats detected by an antivirus product. These are returned as an array of objects. There are more properties than below but this is just an example
file,md5
bad.exe,adfdfdfd
evil.exe,fdfdffdf
I would like to add each member as properties to the custom object so the final output is similar to this.
name,model,sn,01_file,01_md5,02_file,02_md5
Currently, my script does this:
foreach($device in $devices){
$threats = Get-Threats $device
[pscustomobject] #{
name = $device.device_name
make = $device.make
sn = $device.sn
ThreatFileName = $threats.File -join ", "
Threat_md5 = $threats.md5 -join ", "
}
}
This works ok but I'd really like each object returned by the 'Get-Threats' function to be listed as its own set of properties. I need this to be generated dynamically because I don't know how many threats will be returned for each device.
Any thoughts on how to go about this?

You can add properties to objects at any time with the Add-Member cmdlet. Maybe start with an empty object and loop through the elements returned by Get-Threats, adding a member each time?
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/add-member?view=powershell-6
Edit: Example code to answer for reference.
$o = [pscustomobject]#{
MemberA='aaa'
MemberB='bbb'
MemberC='ccc'
MemberD='ddd'
}
"Before"
$o | ft
$o | Add-Member -MemberType NoteProperty -Name 'MemberE' -Value 'eee'
$o | Add-Member -MemberType NoteProperty -Name 'MemberF' -Value 'fff'
"After"
$o | ft

The answer from #krome got me pointed in the right direction although that answer wouldn't work for me as there could be multiple threats for each device.
I used the answer from #scobi on Dynamically get PSCustomObject property and values to arrive at this answer which meets my requirement that the new properties be generated dynamically.
foreach($device in $devices){
$threats = Get-Threats $device
if($null -ne $threats){
$i = 1
foreach($threat in $threats){
$threat | Get-Member -MemberType NoteProperty | % Name | %{
Add-Member -InputObject $device -NotePropertyName ("Threat"+$i.ToString() + "_" + $_) -NotePropertyValue $threat.$_ -Force
}
$i++
}
}
}
Write-Output $devices
I loop over each device in the devices array and call my
Get-Threats function.
The if statement prevents the loop from running for any devices
that don't have threats.
$i is used as my counter to increment the property name for each
threat found so the properties will all have unique names
I then loop over each threat found piping to Get-Member to retrieve
the property name and values
I use Add-Member to add additional properties for each threat found
to each device in the loop, using the counter to give each propery a unique name

Related

Blank records when returning ArrayList of PSObjects from function

So I'm refactoring a Powershell script and moving a lot of stuff into functions. When I return an ArrayList of 42 PSObjects from a function (called Get-OutList) with return $out, 42 blank records are inserted into the beginning of the ArrayList, and my original records then follow.
My function looks like this:
function Get-OutList {
param (
[Parameter(Position=0,mandatory=$true)]
[PSObject]$RoleCollection,
[Parameter(Position=1,mandatory=$true)]
[PSObject]$MemberCollection
)
$out = New-Object System.Collections.ArrayList
$MemberCollection.result | ForEach-Object {
$currentMember = $_
$memberDetail = New-Object PSObject
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name FirstName -Value $($currentMember.user.first_name)
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name LastName -Value $($currentMember.user.last_name)
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Email -Value $($currentMember.user.email)
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Status -Value $($currentMember.status)
$RoleCollection.result | ForEach-Object {
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name $_.name -Value (&{If($currentMember.roles.name -contains $_.name){"Y"}Else{""}})
}
$out.Add($memberDetail)
}
return $out
}
I understand Powershell enumerates each record back to where the function was called, and I've tried a few things to no avail:
Behaviour is the same in PS v5.1.x and v7.3.0.
Returning with return #($out) makes no difference (i.e. results in a System.Object with 42 blank records followed by my 42 original records for a total of 84 records).
Returning with return Write-Output -NoEnumerate $out results in a System.Object with 42 blank records and my 42 original records nested in a 43rd record.
Typing the result of the function call as ArrayList with $results = [System.Collections.ArrayList](Get-OutList) makes no difference.
Why can't I get my object to be the same as before it's returned from a function?? Any assistance would be greatly appreciated!
Edit #1
Including an easily-reproducible example:
function Get-OutList {
param (
[Parameter(Position=0,mandatory=$true)]
[PSObject]$MemberCollection
)
$out = New-Object 'System.Collections.ArrayList'
$MemberCollection | ForEach-Object {
$memberDetail = New-Object PSObject
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name FirstName -Value "One"
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name LastName -Value "Two"
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Email -Value "Three"
Add-Member -InputObject $memberDetail -MemberType NoteProperty -Name Status -Value "Four"
$out.Add($memberDetail)
}
return $out
}
$members = #("Joe Bloggs", "Some Dude", "The Dude")
$memberDetails = Get-OutList -MemberCollection $members
Write-Output $memberDetails
If you add a breakpoint before $out is passed back, you'll see there are three records, and if you keep stepping, you should see $memberDetails will have six records (the first three blank).
Edit #2
Appears there's no such problem when using Generic.List instead of ArrayList. Used $out = [System.Collections.Generic.List[PSObject]]::new() instead of $out = New-Object 'System.Collections.ArrayList' and it's working just fine.
Your problem stems from the fact that the System.Collections.ArrayList.Add() method has a return value (the index at which the object has been added) and that that return value becomes part of your function's "return value", i.e. stream of output objects.
This is a manifestation of PowerShell's implicit output behavior, which is explained in this answer.
Notably, return is not needed in PowerShell to produce output from a function - any statement can produce output. return $foo is simply syntactic sugar for Write-Output $foo; return or, relying on implicit output behavior, $foo; return
Therefore, the immediate fix to your problem is to discard (suppress) the implicit output produced by your $out.Add($memberDetail) call, which is typically done by assigning the call to $null (there are other methods, discussed in this answer):
# Discard the unwanted output from .Add()
$null = $out.Add($memberDetail)
In your own answer, you indirectly applied that fix by switching to the System.Collections.Generic.List`1 list type, whose .Add() method has no return value. It is for that reason, along with the ability to strongly type the list elements, that System.Collections.Generic.List`1 is generally preferable to System.Collections.ArrayList
Taking a step back:
In simple cases such as yours, where the entire function output is to be captured in a list, you can take advantage of the output-stream behavior and simply emit each output object individually from your function (from the ForEach-Object script block) and let PowerShell collect the individual objects emitted in a list-like data structure, which will be a regular PowerShell array, namely of type [object[] (i.e., a fixed-size analog to System.Collections.ArrayList):
This is not only more convenient, but also performs better.
Applied to your sample function:
function Get-OutList {
param (
[Parameter(Position=0,mandatory=$true)]
[PSObject]$MemberCollection
)
$MemberCollection | ForEach-Object {
$first, $last = -split $_
# Construct and implicitly output a custom object.
[pscustomobject] #{
FirstName = $first
LastName = $last
Email = 'Three'
Status = 'Four'
}
}
# No need for `return` - all output objects were emitted above.
}
$members = #("Joe Bloggs", "Some Dude", "The Dude")
# By assigning the function's output stream to a variable,
# the individual output objects are automatically collected in an array
# (assuming *two or more* output objects; if you want an array even if
# only *one* object is output, use [Array] $memberDetails = ...)
$memberDetails = Get-OutList -MemberCollection $members
# Output (implicitly).
$memberDetails
Also note the use of the [pscustomobject] #{ ... } syntactic sugar for simplified custom-object creation - see the conceptual about_Object_Creation help topic.
Used $out = [System.Collections.Generic.List[PSObject]]::new() instead of $out = New-Object 'System.Collections.ArrayList' and it's working just fine. No extra blankies.

Powershell pscustomobject with array properties to Csv file

I got an $Object with a ton of properties which look like this:
IsSynchronized : { False, False }
What I want is to do something like :
$Object | Export-Csv C:\Test\Merge.csv -Delimiter ';'
To get a CSV containing :
IsSynchronized
--------------
False
False
But as expected I get
IsSynchronized
---------------
System.Object[]
Is there a good way to get a ton of the object's properties in a .CSV ?
I have 6 .csv files with values and I try to add them in to one big .csv for further processing.
Edit:
I asked about this Topic yesterday but i need to make more clear what i want.
I create a PSCustomObject and fill it with Arrays:
$Object = New-Object -TypeName PSCustomObject
$Object | add-member -membertype NoteProperty -name "CPUHost" -value $global:CPUHost
$Object | add-member -membertype NoteProperty -name "NumCpu" -value $global:NumCpu
$Object | add-member -membertype NoteProperty -name "MemoryMB" -value $global:MemoryMB
$Object | add-member -membertype NoteProperty -name "CPU Usage (Average), Mhz" -value $global:CPUUsageAverageMhz
$Object | add-member -membertype NoteProperty -name "CPU Usage (Average), %" -value $global:CPUUsageAverage
$Object | add-member -membertype NoteProperty -name "Memory Usage (Average), %" -value $global:MemoryUsageAverage
$Object | add-member -membertype NoteProperty -name "Network Usage (Average), KBps" -value $global:NetworkUsageAverageKBps
$Object | add-member -membertype NoteProperty -name "Disk Usage (Average), KBps" -value $global:DiskUsageAverageKBps
...
All of these Global Variables are Arrays because i never know how many Values i get in the first place.
They are filled by lopping through 6 CSV Files i will allways get.
After running this bit i will have a Object looking like this:
CPUHost : {xxxx}
NumCpu : {20}
MemoryMB : {36094}
CPU Usage (Average), Mhz : {3914,33}
CPU Usage (Average), % : {8,91}
Memory Usage (Average), % : {70,17}
Network Usage (Average), KBps : {439,68}
Disk Usage (Average), KBps : {1994,93}
...
What i want is to Export that in to a CSV Displayed like :
CPUHost NumCPU MemoryMb CPUUsage ...
------- ------ -------- ---------
xxxx 20 36094 3914
33
With every Value in its own Cell.
What i get is instead of the values : System.Type.[] which is technically correct but not what i need.
I allready tryed to -join the values but that will leave me with the values in the same cell
You state that you have one object with multiple properties like what was posted. I assume you have an object that looks like the following:
IsSynchronized Property2 Property3
-------------- --------- ---------
{False, False} {1, 2, 3} {string1, string2, string3, string4}
You could do the following:
$loopmax = $object[0].psobject.properties |% {($_.Value | measure-object).Count} | Sort -desc | Select -First 1
$newobject = for ($i = 0; $i -lt $loopmax; $i++) {
$hash = [ordered]#{}
foreach ($p in $object[0].psobject.properties.name) {
$hash[$p] = $object.$p[$i]
}
[pscustomobject]$hash
}
$newobject | convertto-csv -notype
This doesn't seem wise to do. CSV isn't very good for representing or storing rich or hierarchical objects. The problem is even worse when the property is an object, in your case it's a flat array. You can output as you suggest / request, and other answers have demonstrated:
IsSynchronized
--------------
False
False
However, this disrupts the property's relationship to the other properties. If you have other types like [String] & [Int], or even varying numbers of elements in array typed properties things are going to get weird quickly!
If you must stick with CSV you can sub-delimit the field. A great example of this is Exchange Message Tracking logs. They are CSV files delimited on the typical ",", but the recipients field is sub-delimited on a ";".
An example in code might look something like this:
$Object =
[PSCustomObject]#{
Prop1 = "one"
Prop2 = "two"
Arr1 = #( 1,2,3,4 )
}
$Object |
Select-Object Prop1, Prop2,
#{ Name = 'Arr1'; Expression = {$_.Arr1 -join "," } } |
ConvertTo-Csv -Delimiter ";"
Results:
"Prop1";"Prop2";"Arr1"
"one";"two";"1,2,3,4"
Note: To use full fidelity data in a later process would require appropriate handling on input. However, if you follow other solutions and said weirdness occurs you'll be left with a similar issue; having to handle on the input side everywhere you intend to use that data.
Given CSV's shortcomings JSON may be a better choice to store & reuse full fidelity objects. Export/Import CliXML are interesting for this.
To answer the literal question asked (although I'm not sure that's what the OP actually wants)...
If you have a single object with a property that contains an array of values, you can expand them out into a new array and then convert that to csv as follows:
$obj = new-object psobject -Property #{
"IsSynchronized" = #( $false, $false )
}
$obj
# IsSynchronized
# --------------
# {False, False}
$data = $obj.IsSynchronized | foreach-object {
new-object pscustomobject -Property #{ "IsSynchronized" = $_ }
}
$csv = $data | ConvertTo-Csv -NoTypeInformation
$csv
# "IsSynchronized"
# "False"
# "False"

Get the term set name that is being used in sharepoint sites

I have been tasked to get the MMS term sets that are being used based on terms(not with null value of MMS column in the list's items ) in all the sites so that only those MMS terms sets can get migrated to the other sharepoint environment. On a base level I'm using below script
$FieldCollection= (Get-SPWeb https:/sharepoint.com/sites/pssl/mgmt).Lists.Fields
$MetadataField = New-Object psobject
$MetadataField | Add-Member -MemberType NoteProperty -Name "ParentListUrl" -value ""
$MetadataField | Add-Member -MemberType NoteProperty -Name "ParentListTitle" -value ""
$MetadataField | Add-Member -MemberType NoteProperty -Name "FieldTitle" -value ""
$MetadataField | Add-Member -MemberType NoteProperty -Name "FieldId" -value ""
$matches = #();
foreach($field in $FieldCollection)
{
if($field.GetType().Name -ne "TaxonomyField"){
continue;
}
#if($field.TermSetId.ToString() -ne $TermSet.Id.ToString()){continue;}
$tf = $MetadataField | Select-Object *;
$tf.ParentListUrl = $field.ParentList.ParentWeb.Url;
$tf.ParentListTitle = $field.ParentList.Title;
$tf.FieldTitle = $field.Title;
$tf.FieldId = $field.ID;
$matches += $tf;
}
return $matches;
but it returns only managed metadata columns defined in the list, but not they are being used in the list. Can anybody help me to achieve the task.
I'm not an expert in Sharepoint API, but I'm trying to understand what the problem is to help you and I can't.
I notice that you create an object $MetadataField before a loop, then kind of create a replica $MetadataField | Select-Object * and then add in a array.
As you say, the returned objects should only have the columns of ParentListUrl,ParentListTitle,FieldTitle and FieldId which is what I expect from the sample above. Can you elaborate more on what you are looking for? Maybe update the entire function into your question and post your returned expectation. this way I can try to help you out.
Btw, the ; is not required and you should create a new instance of the object within the loop. You can use the same method or first create a hash key that drives the properties of a custom object. For example in your loop adjust the following.
$hash=#{
Property1="Value1"
Property2="Value2"
}
New-Object -Type PSObject -Property $hash
Also if you function returns directly each found item without extra processing, then you can skip adding them in an array and just write in the output like I do in my example. To make it more clear, if I would put a loop around my example in a function and execute, then I would get a recordset with custom object with Property1 and Property2

Change order of columns in the object

How can I change the column ordering of the output my code produces:
$apps = Import-CSV apps.csv
$computers = Import-CSV compobj.csv
foreach ($computer in $computers) {
$computerLob = $computer.lob
$lobApps = $apps | ? {$_.lob -eq $computerLob }
foreach ($app in $lobApps) {
$computerHostname = $computer.hostname
$appLocation = $app.location
$installed=Test-Path "\\$computerHostname\$appLocation"
New-Object PSObject -Property #{
Computer=$computer.hostname
App=$app.appname
Installed=$installed
}
}
Currently it's producing the columns in the following order: Installed,App,Computer.
I'd like to have it in the following order: Computer,App,Installed.
Powershell V3 added a type accelerator for [PSCustomObject] that creates objects using an ordered hash table, so the properties stay in the order they're declared:
[PSCustomObject] #{
Computer=$computer.hostname
App=$app.appname
Installed=$installed
}
If you want to ensure the output of an object occurs in a certain order i.e. formatted a certain way then use PowerShell's formatting commands.
$obj = [pscustomobject]#{Computer=$computer.hostname;App=$app.appname;Installed=$installed}
$obj | Format-Table Computer,App,Installed
What's more you can better control the output e.g.:
$obj | Format-Table Computer,App,Installed -AutoSize
Or control field width & alignment:
$obj | Format-Table #{label='Computer';expression={$_.Computer};width=20;alignment='right'},App,Installed
Frankly I think it is a better practice to use the formatting commands to get the output order you want rather than to rely upon the order in which the properties are created.
The problem is that you're adding the properties using a hashtable, and hashtables don't preserve order. You can do one of two things:
1. Add the properties in a manner that will set the order (the last line is to output the object to the pipeline):
$obj = New-Object PSObject
$obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer.hostname
$obj | Add-Member -MemberType NoteProperty -Name App -Value $app.appname
$obj | Add-Member -MemberType NoteProperty -Name Installed -Value $installed
$obj
2. Determine the order at the time you output the object to the pipeline using Select-Object:
New-Object PSObject -Property #{
Computer=$computer.hostname
App=$app.appname
Installed=$installed
} | select Computer,App,Installed
Which one is preferable depends on how much you'll be working with the object. If, as your question implies, you're only using the PSObject in order to display the information in tabular format, the second method is quicker. If you're going to output the object multiple times in different parts of the script, the first method allows you to simply output it as $obj rather than having to pipe to select every time.
Note also that the second method can be split up like this if you don't want to output the object immediately after populating it:
$obj = New-Object PSObject -Property #{
Computer=$computer.hostname
App=$app.appname
Installed=$installed
}
[...do other stuff...]
$obj | select Computer,App,Installed
Format-Table it's a good solution when you need to display your object fields in a specific order, but it will change the obect and you won't be able to pipe your object, for example when exporting to csv (Export-Csv).
In the case you just want to change "the order of the fields in the object" use Select-Object. This will preserve object type and fields, and still you will be able to pipe the object to other cmdlets.
A more universal way to change the order is using the Select-Object cmdlet with the list of properties in the required order.
See the example:
PS> $ObjectList = 1..3 |
% {New-Object psobject -Property #{P2="Object $_ property P2"; P1="Object $_ property P1"}}
PS> $ObjectList
P2 P1
-- --
Object 1 property P2 Object 1 property P1
Object 2 property P2 Object 2 property P1
Object 3 property P2 Object 3 property P1
PS> $ObjectList | Select-Object P1,P2
P1 P2
-- --
Object 1 property P1 Object 1 property P2
Object 2 property P1 Object 2 property P2
Object 3 property P1 Object 3 property P2
The full form of these commands is the following:
$ObjectList = 1..3 |
ForEach-Object -Process {New-Object -TypeName psobject -Property #{P2="Object $_ property P2"; P1="Object $_ property P1"}}
$ObjectList | Select-Object -Property P1,P2

How do I create a custom type in PowerShell for my scripts to use?

I would like to be able to define and use a custom type in some of my PowerShell scripts. For example, let's pretend I had a need for an object that had the following structure:
Contact
{
string First
string Last
string Phone
}
How would I go about creating this so that I could use it in function like the following:
function PrintContact
{
param( [Contact]$contact )
"Customer Name is " + $contact.First + " " + $contact.Last
"Customer Phone is " + $contact.Phone
}
Is something like this possible, or even recommended in PowerShell?
Prior to PowerShell 3
PowerShell's Extensible Type System didn't originally let you create concrete types you can test against the way you did in your parameter. If you don't need that test, you're fine with any of the other methods mentioned above.
If you want an actual type that you can cast to or type-check with, as in your example script ... it cannot be done without writing it in C# or VB.net and compiling. In PowerShell 2, you can use the "Add-Type" command to do it quite simmple:
add-type #"
public struct contact {
public string First;
public string Last;
public string Phone;
}
"#
Historical Note: In PowerShell 1 it was even harder. You had to manually use CodeDom, there is a very old function new-struct script on PoshCode.org which will help. Your example becomes:
New-Struct Contact #{
First=[string];
Last=[string];
Phone=[string];
}
Using Add-Type or New-Struct will let you actually test the class in your param([Contact]$contact) and make new ones using $contact = new-object Contact and so on...
In PowerShell 3
If you don't need a "real" class that you can cast to, you don't have to use the Add-Member way that Steven and others have demonstrated above.
Since PowerShell 2 you could use the -Property parameter for New-Object:
$Contact = New-Object PSObject -Property #{ First=""; Last=""; Phone="" }
And in PowerShell 3, we got the ability to use the PSCustomObject accelerator to add a TypeName:
[PSCustomObject]#{
PSTypeName = "Contact"
First = $First
Last = $Last
Phone = $Phone
}
You're still only getting a single object, so you should make a New-Contact function to make sure that every object comes out the same, but you can now easily verify a parameter "is" one of those type by decorating a parameter with the PSTypeName attribute:
function PrintContact
{
param( [PSTypeName("Contact")]$contact )
"Customer Name is " + $contact.First + " " + $contact.Last
"Customer Phone is " + $contact.Phone
}
In PowerShell 5
In PowerShell 5 everything changes, and we finally got class and enum as language keywords for defining types (there's no struct but that's ok):
class Contact
{
# Optionally, add attributes to prevent invalid values
[ValidateNotNullOrEmpty()][string]$First
[ValidateNotNullOrEmpty()][string]$Last
[ValidateNotNullOrEmpty()][string]$Phone
# optionally, have a constructor to
# force properties to be set:
Contact($First, $Last, $Phone) {
$this.First = $First
$this.Last = $Last
$this.Phone = $Phone
}
}
We also got a new way to create objects without using New-Object: [Contact]::new() -- in fact, if you kept your class simple and don't define a constructor, you can create objects by casting a hashtable (although without a constructor, there would be no way to enforce that all properties must be set):
class Contact
{
# Optionally, add attributes to prevent invalid values
[ValidateNotNullOrEmpty()][string]$First
[ValidateNotNullOrEmpty()][string]$Last
[ValidateNotNullOrEmpty()][string]$Phone
}
$C = [Contact]#{
First = "Joel"
Last = "Bennett"
}
Creating custom types can be done in PowerShell.
Kirk Munro actually has two great posts that detail the process thoroughly.
Naming Custom Objects
Defining Default Properties for Custom Objects
The book Windows PowerShell In Action by Manning also has a code sample for creating a domain specific language to create custom types. The book is excellent all around, so I really recommend it.
If you are just looking for a quick way to do the above, you could create a function to create the custom object like
function New-Person()
{
param ($FirstName, $LastName, $Phone)
$person = new-object PSObject
$person | add-member -type NoteProperty -Name First -Value $FirstName
$person | add-member -type NoteProperty -Name Last -Value $LastName
$person | add-member -type NoteProperty -Name Phone -Value $Phone
return $person
}
This is the shortcut method:
$myPerson = "" | Select-Object First,Last,Phone
Steven Murawski's answer is great, however I like the shorter (or rather just the neater select-object instead of using add-member syntax):
function New-Person() {
param ($FirstName, $LastName, $Phone)
$person = new-object PSObject | select-object First, Last, Phone
$person.First = $FirstName
$person.Last = $LastName
$person.Phone = $Phone
return $person
}
Surprised no one mentioned this simple option (vs 3 or later) for creating custom objects:
[PSCustomObject]#{
First = $First
Last = $Last
Phone = $Phone
}
The type will be PSCustomObject, not an actual custom type though. But it is probably the easiest way to create a custom object.
There is the concept of PSObject and Add-Member that you could use.
$contact = New-Object PSObject
$contact | Add-Member -memberType NoteProperty -name "First" -value "John"
$contact | Add-Member -memberType NoteProperty -name "Last" -value "Doe"
$contact | Add-Member -memberType NoteProperty -name "Phone" -value "123-4567"
This outputs like:
[8] ยป $contact
First Last Phone
----- ---- -----
John Doe 123-4567
The other alternative (that I'm aware of) is to define a type in C#/VB.NET and load that assembly into PowerShell for use directly.
This behavior is definitely encouraged because it allows other scripts or sections of your script work with an actual object.
Here is the hard path to create custom types and store them in a collection.
$Collection = #()
$Object = New-Object -TypeName PSObject
$Object.PsObject.TypeNames.Add('MyCustomType.Contact.Detail')
Add-Member -InputObject $Object -memberType NoteProperty -name "First" -value "John"
Add-Member -InputObject $Object -memberType NoteProperty -name "Last" -value "Doe"
Add-Member -InputObject $Object -memberType NoteProperty -name "Phone" -value "123-4567"
$Collection += $Object
$Object = New-Object -TypeName PSObject
$Object.PsObject.TypeNames.Add('MyCustomType.Contact.Detail')
Add-Member -InputObject $Object -memberType NoteProperty -name "First" -value "Jeanne"
Add-Member -InputObject $Object -memberType NoteProperty -name "Last" -value "Doe"
Add-Member -InputObject $Object -memberType NoteProperty -name "Phone" -value "765-4321"
$Collection += $Object
Write-Ouput -InputObject $Collection
Here's one more option, which uses a similar idea to the PSTypeName solution mentioned by Jaykul (and thus also requires PSv3 or above).
Example
Create a TypeName.Types.ps1xml file defining your type. E.g. Person.Types.ps1xml:
<?xml version="1.0" encoding="utf-8" ?>
<Types>
<Type>
<Name>StackOverflow.Example.Person</Name>
<Members>
<ScriptMethod>
<Name>Initialize</Name>
<Script>
Param (
[Parameter(Mandatory = $true)]
[string]$GivenName
,
[Parameter(Mandatory = $true)]
[string]$Surname
)
$this | Add-Member -MemberType 'NoteProperty' -Name 'GivenName' -Value $GivenName
$this | Add-Member -MemberType 'NoteProperty' -Name 'Surname' -Value $Surname
</Script>
</ScriptMethod>
<ScriptMethod>
<Name>SetGivenName</Name>
<Script>
Param (
[Parameter(Mandatory = $true)]
[string]$GivenName
)
$this | Add-Member -MemberType 'NoteProperty' -Name 'GivenName' -Value $GivenName -Force
</Script>
</ScriptMethod>
<ScriptProperty>
<Name>FullName</Name>
<GetScriptBlock>'{0} {1}' -f $this.GivenName, $this.Surname</GetScriptBlock>
</ScriptProperty>
<!-- include properties under here if we don't want them to be visible by default
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
</Members>
</MemberSet>
-->
</Members>
</Type>
</Types>
Import your type: Update-TypeData -AppendPath .\Person.Types.ps1xml
Create an object of your custom type: $p = [PSCustomType]#{PSTypeName='StackOverflow.Example.Person'}
Initialise your type using the script method you defined in the XML: $p.Initialize('Anne', 'Droid')
Look at it; you'll see all properties defined: $p | Format-Table -AutoSize
Type calling a mutator to update a property's value: $p.SetGivenName('Dan')
Look at it again to see the updated value: $p | Format-Table -AutoSize
Explanation
The PS1XML file allows you to define custom properties on types.
It is not restricted to .net types as the documentation implies; so you can put what you like in '/Types/Type/Name' any object created with a matching 'PSTypeName' will inherit the members defined for this type.
Members added through PS1XML or Add-Member are restricted to NoteProperty, AliasProperty, ScriptProperty, CodeProperty, ScriptMethod, and CodeMethod (or PropertySet/MemberSet; though those are subject to the same restrictions). All of these properties are read only.
By defining a ScriptMethod we can cheat the above restriction. E.g. We can define a method (e.g. Initialize) which creates new properties, setting their values for us; thus ensuring our object has all the properties we need for our other scripts to work.
We can use this same trick to allow the properties to be updatable (albeit via method rather than direct assignment), as shown in the example's SetGivenName.
This approach isn't ideal for all scenarios; but is useful for adding class-like behaviors to custom types / can be used in conjunction with other methods mentioned in the other answers. E.g. in the real world I'd probably only define the FullName property in the PS1XML, then use a function to create the object with the required values, like so:
More Info
Take a look at the documentation, or the OOTB type file Get-Content
$PSHome\types.ps1xml for inspiration.
# have something like this defined in my script so we only try to import the definition once.
# the surrounding if statement may be useful if we're dot sourcing the script in an existing
# session / running in ISE / something like that
if (!(Get-TypeData 'StackOverflow.Example.Person')) {
Update-TypeData '.\Person.Types.ps1xml'
}
# have a function to create my objects with all required parameters
# creating them from the hash table means they're PROPERties; i.e. updatable without calling a
# setter method (note: recall I said above that in this scenario I'd remove their definition
# from the PS1XML)
function New-SOPerson {
[CmdletBinding()]
[OutputType('StackOverflow.Example.Person')]
Param (
[Parameter(Mandatory)]
[string]$GivenName
,
[Parameter(Mandatory)]
[string]$Surname
)
([PSCustomObject][Ordered]#{
PSTypeName = 'StackOverflow.Example.Person'
GivenName = $GivenName
Surname = $Surname
})
}
# then use my new function to generate the new object
$p = New-SOPerson -GivenName 'Simon' -Surname 'Borg'
# and thanks to the type magic... FullName exists :)
Write-Information "$($p.FullName) was created successfully!" -InformationAction Continue