Using PropertySets

With PropertySets, you control the visible default properties of your PowerShell functions. Learn how PowerShell makes sure users aren't overwhelmed with information.

An object-oriented shell like PowerShell was long thought impossible because objects typically contain a wealth of information, and Administrators would be overwhelmed with information if all object properties would always be outputted to the shell.


I started this article because in a recent tip I created a function called Get-Software which returns tons of information about installed programs - in fact so much information that it simply felt overwhelming to most users and the common daily scenarios.

That’s why I added a way for the function to return only a few default properties while the user was still able to use any hidden property when needed. This technique is pretty slick but not widely known, and it is not the only one available. So I decided to dive a bit into the PowerShell type system and look at it from a users perspective, then from an authors perspective.

This article is split in two parts:

  • In the first part, I’ll explain what the PowerShell type system is and how it helps you every day. That’s valuable for beginners or auto-didacts that want to complete their knowledge. Seasoned PowerShell veterans can skip this part.
  • In the second part, you switch perspective: rather than consuming the PowerShell type system like a end user does, you act as an author and begin creating your own PowerShell functions and modules that build on top of the PowerShell type system and actively embrace it. This way, your own PowerShell functions may become a lot more usable for your end users.

Voilá: The PowerShell Type System

PowerShell is one of the first fully object-oriented shells around, and there is a reason why most other shells are not: in order for an object-oriented shell to be manageable, you need an object visualizer. One of the key jobs for this visualizer is to protect the user from information overflow.

Whenever you output objects to the PowerShell console, they get converted to text, and the internal type system makes sure you see only a subset of all available properties. By limiting the output to only a few properties, Admins can continue to focus on what’s important.

Have a look: this command returns a file:

Get-Item -Path c:\windows\explorer.exe

Thanks to the internal PowerShell type system, the output is restricted to only the typically used properties which look familiar to Admins:

    Directory: C:\windows

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       01.03.2020     10:38        4622280 explorer.exe

Looking At Pure Objects

In reality, Get-Item always returns rich .NET objects. As long as you pipe the results to another command or save the results to a variable, the .NET object(s) stay intact. The stripping occurs only when you output the information to the console

By appending Select-Object before the objects are outputted, you can decide which properties you want to see. Use “*” to see all properties:

Get-Item -Path c:\windows\explorer.exe | Select-Object -Property *

Actually, Select-Object creates an object clone and essentially strips off all type information.

# original object has distinct type information attached to it:
$file = Get-Item -Path c:\windows\explorer.exe

So it is not Select-Object that “overrules” the type system. Rather, any custom object with missing type information shows all of its properties:

# Cloned objects returned by Select-Object change the type of the object,
# and since these generic types do not exist in the hint list 
# of the type system, all properties are shown:
$file = Get-Item -Path c:\windows\explorer.exe | Select-Object -Property *

Get-Item returns the very same information. Now, though, way more properties are shown, and because they wouldn’t fit horizontally, PowerShell automatically switches from table view to list view.

PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\windows\explorer.exe
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName       : explorer.exe
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : False
Mode              : -a----
VersionInfo       : File:             C:\windows\explorer.exe
                    InternalName:     explorer
                    OriginalFilename: EXPLORER.EXE.MUI
                    FileVersion:      10.0.18362.650 (WinBuild.160101.0800)
                    FileDescription:  Windows Explorer
                    Product:          Microsoft® Windows® Operating System
                    ProductVersion:   10.0.18362.650
                    Debug:            False
                    Patched:          False
                    PreRelease:       False
                    PrivateBuild:     False
                    SpecialBuild:     False
                    Language:         English (United States)
BaseName          : explorer
Target            : {C:\Windows\WinSxS\amd64_microsoft-windows-explorer_31bf3856ad364e35_10.0.18362.693_none_a7549da20b808a40\explorer.exe}
LinkType          : HardLink
Name              : explorer.exe
Length            : 4622280
DirectoryName     : C:\windows
Directory         : C:\windows
IsReadOnly        : False
Exists            : True
FullName          : C:\windows\explorer.exe
Extension         : .exe
CreationTime      : 01.03.2020 10:38:51
CreationTimeUtc   : 01.03.2020 09:38:51
LastAccessTime    : 01.03.2020 10:38:51
LastAccessTimeUtc : 01.03.2020 09:38:51
LastWriteTime     : 01.03.2020 10:38:51
LastWriteTimeUtc  : 01.03.2020 09:38:51
Attributes        : Archive

Working With Objects

Because of this clever architecture, PowerShell commands provide a quick overview but you always keep the option to see more. This is important to understand.

In user groups, you often come across questions like “command xyz won’t give me the information abc, where can I get it?”. Most likely, the command returned it already, but it was hidden. So the best answer would be: “append | Select-Object * and check again!”

Let’s take a quick look at the typical PowerShell workflow:

1. Run Command

First, run a command and look at the default output, for example:

Get-Process -Id $pid

Take a look at the results to see if the information surfaces that you need:

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
   1235      93   580128     647576      53,30  21500   1 powershell_ise

2. Look Again With All Properties Visible

If the command won’t give you the information you need, run it through | Select-Object * -First 1. This gives you all properties for the first (random) returned object.

Without -First 1 you’d see a long list of repeating information if the command returned many objects. To investigate properties, it’s better to focus on one object.

Get-Process -Id $pid | Select-Object -Property * -First 1

Look at the full range of properties, and take a note of any that are useful to you:

Name                       : powershell_ise
Id                         : 21500
PriorityClass              : Normal
FileVersion                : 10.0.18362.1 (WinBuild.160101.0800)
HandleCount                : 1255
WorkingSet                 : 651796480
PagedMemorySize            : 590516224
PrivateMemorySize          : 590516224
VirtualMemorySize          : 1844117504
TotalProcessorTime         : 00:00:54.0937500
SI                         : 1
Handles                    : 1255
VM                         : 6139084800
WS                         : 651796480
PM                         : 590516224
NPM                        : 94384
Path                       : C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell_ISE.exe
Company                    : Microsoft Corporation
CPU                        : 54,109375
ProductVersion             : 10.0.18362.1
Description                : Windows PowerShell ISE
Product                    : Microsoft® Windows® Operating System
__NounName                 : Process
BasePriority               : 8
ExitCode                   : 
HasExited                  : False
ExitTime                   : 
Handle                     : 4004
SafeHandle                 : Microsoft.Win32.SafeHandles.SafeProcessHandle
MachineName                : .
MainWindowHandle           : 8192226
MainWindowTitle            : C:\Users\tobia
MainModule                 : System.Diagnostics.ProcessModule (PowerShell_ISE.exe)
MaxWorkingSet              : 1413120
MinWorkingSet              : 204800
Modules                    : {System.Diagnostics.ProcessModule (PowerShell_ISE.exe), System.Diagnostics.ProcessModule (ntdll.dll), System.Diagnostics.ProcessModule (MSCOREE.DLL), 
                             System.Diagnostics.ProcessModule (KERNEL32.dll)...}
NonpagedSystemMemorySize   : 94384
NonpagedSystemMemorySize64 : 94384
PagedMemorySize64          : 590516224
PagedSystemMemorySize      : 1645200
PagedSystemMemorySize64    : 1645200
PeakPagedMemorySize        : 721035264
PeakPagedMemorySize64      : 721035264
PeakWorkingSet             : 806756352
PeakWorkingSet64           : 806756352
PeakVirtualMemorySize      : 1941008384
PeakVirtualMemorySize64    : 6235975680
PriorityBoostEnabled       : True
PrivateMemorySize64        : 590516224
PrivilegedProcessorTime    : 00:00:11.7968750
ProcessName                : powershell_ise
ProcessorAffinity          : 255
Responding                 : True
SessionId                  : 1
StartInfo                  : System.Diagnostics.ProcessStartInfo
StartTime                  : 28.03.2020 23:08:17
SynchronizingObject        : 
Threads                    : {24448, 20968, 4456, 18388...}
UserProcessorTime          : 00:00:42.3281250
VirtualMemorySize64        : 6139084800
EnableRaisingEvents        : False
StandardInput              : 
StandardOutput             : 
StandardError              : 
WorkingSet64               : 651796480
Site                       : 
Container                  :

3. Fine-Tune Select-Object

Replace “*” with the comma-separated list of properties you find useful:

Get-Process -Id $pid | Select-Object -Property Name, Description, MainWindowTitle, StartTime

When you compare the result with the default result you got in step 1, you immediately understand how flexible PowerShell commands are: the very same Get-Process command now provides you with a completely different output:

Name           Description            MainWindowTitle StartTime          
----           -----------            --------------- ---------          
powershell_ise Windows PowerShell ISE C:\Users\tobia  28.03.2020 23:08:17

4. Look At Parameters That Unlock Information

If you still can’t see the properties you need, as a last resort look at the cmdlet parameters to see if there is any that can add more information.

For example, if you wanted to find out the owner of a process, it turns out that Get-Process adds this information only on request (because it is a rather expensive operation, plus it requires Admin privileges):

Get-Process -Id $pid -IncludeUserName

Provided you ran the command with elevated privileges, you now see a property that wasn’t there before: UserName.

Handles      WS(K)   CPU(s)     Id UserName               ProcessName
-------      -----   ------     -- --------               -----------
    848     297024     6,14   4644 DESKTOP-8DVNI43\tobia  powershell_ise

Whenever it is especially expensive to retrieve or calculate information, it is likely that there are distinct properties that need to be set.

For example, if you want to see all properties of Active Directory users returned by Get-AdUser, you need to add the parameter -Properties * to the command. Else, you get only a few basic properties.

Behind The Scenes: Out-Default

All of this occurs when objects are outputted to the PowerShell console. Let’s take a quick look at the architecture of this part.

PowerShell checks whether your command is a variable assignment, and if it is not, it secretly appends Out-Default to your command. Out-Default is the magic command that takes the object, strips properties, and returns a rich text representation.

Here are a couple of fun proof-of-concepts that illustrate how this all works:

Silencing Output

When you overwrite Out-Default with a function that does not return anything, you can silence PowerShell, and no output is written to the console anymore:

# silence output
function Out-Default {}
# no output:

Likewise, when you output anything from your version of Out-Default, this is what gets outputted instead of the original output whenever you run a command:

# overwrite output
filter Out-Default { "censored" }
# returns "censored" per each service (run lines individually!)

A filter executes the code for each incoming pipeline element. If you replace filter by function, you get only one “censored” for all services.

Bypassing Formatting System

When your version of Out-Default echoes back the incoming objects, you are bypassing the formatting system:

filter Out-Default { $_ }

You now witness how PowerShell would behave if objects were converted to text using only their built-in method ToString():


The result is a plain text list of service names:


Bypassing Type System

Finally, let’s replace Out-Default with a more sophisticated proxy function that strips all type information from objects before they are processed:

function Out-Default


    # prepend a foreach-object, and strip off all type information from incoming objects before
    # calling the "real" Out-Default with its parameters:
    $scriptCmd = {& Foreach-Object { $_.PSTypeNames.Clear(); $_ } | Microsoft.PowerShell.Core\Out-Default @PSBoundParameters }
    # use a steppable pipeline to use "our" pipeline in place of the original command:
    $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)

  # do default actions for any incoming object:
  process { $steppablePipeline.Process($_) }
  # do default action when pipeline is done:
  end { $steppablePipeline.End() }

When you run this code, Out-Default now is internally replaced by this command:

| Foreach-Object { $_.PSTypeNames.Clear(); $_ } | Out-Default

Before objects get to the original Out-Default, the type information in the hidden property PSTypeNames is cleared. Without the type information, the PowerShell type system is “blinded” and no longer finds the types in his hinting tables. So now you always get to see all properties.

Just imagine PowerShell would always look and work like this - scary thought:

dir c:\windows
PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\windows\addins
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName       : addins
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : True
Name              : addins
FullName          : C:\windows\addins
Parent            : windows
Exists            : True
Root              : C:\
Extension         : 
CreationTime      : 19.03.2019 05:52:44
CreationTimeUtc   : 19.03.2019 04:52:44
LastAccessTime    : 26.08.2019 18:59:41
LastAccessTimeUtc : 26.08.2019 16:59:41
LastWriteTime     : 19.03.2019 05:52:52
LastWriteTimeUtc  : 19.03.2019 04:52:52
Attributes        : Directory

PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\windows\ADFS
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName       : ADFS
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : True
Name              : ADFS
FullName          : C:\windows\ADFS
Parent            : windows
Exists            : True
Root              : C:\
Extension         : 
CreationTime      : 12.11.2019 16:14:51
CreationTimeUtc   : 12.11.2019 15:14:51
LastAccessTime    : 12.11.2019 16:14:51
LastAccessTimeUtc : 12.11.2019 15:14:51
LastWriteTime     : 12.11.2019 16:14:51
LastWriteTimeUtc  : 12.11.2019 15:14:51
Attributes        : Directory

PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\windows\appcompat
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName       : appcompat
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : True
Name              : appcompat
FullName          : C:\windows\appcompat
Parent            : windows
Exists            : True
Root              : C:\
Extension         : 
CreationTime      : 19.03.2019 05:52:44
CreationTimeUtc   : 19.03.2019 04:52:44
LastAccessTime    : 08.09.2019 12:58:54
LastAccessTimeUtc : 08.09.2019 10:58:54
LastWriteTime     : 08.09.2019 12:58:42
LastWriteTimeUtc  : 08.09.2019 10:58:42
Attributes        : Directory


At the same time, you get a good clue of how the type system works: any object has a hidden property PSTypeNames that lists the types that describe this object. PowerShell traverses this list and checks whether one of these types is found in its internal hint lists.

There are actually potentially two hint lists for a type: formats and type information. Combined, they define which default properties should be visible, in which format these should appear (i.e. table, or list), and what the default column width and alignment for any property should be.

So PowerShell can fully customize the way how objects are converted to text, and so can you in just a moment.

If so, PowerShell uses the type and formatting hints to convert the object to text.

If the type is not found, all properties are returned, and PowerShell uses a rule-of-thumb universal formatting rule:

  • Objects with less than five properties are formatted as a table.
  • Objects with five or more properties are formatted as a list.

Restoring Out-Default

To remove all customizations and return to the built-in Out-Default behavior, delete your function:

Remove-Item -Path function:Out-Default

PowerShell now uses the built-in cmdlet Out-Default again.

PowerShell functions have a higher precedence than cmdlets. In the examples above, I temporarily overruled the built-in cmdlet ` Out-Default` by defining a function with the same name.

Using Hints For PowerShell Functions

Most PowerShell functions do not take notice of the features I just described. That’s perfectly ok provided your functions return default .NET objects: the PowerShell type system knows how to properly handle these object types regardless of who produced them.

Custom Objects Require Your Love

Once your functions return custom objects, things become messy, though. Custom objects are typically created in one of these ways:

  • Select-Object: When you use Select-Object, the result is always a custom object
  • [PSCustomObject]: When you “convert” a hashtable to [PSCustomObject], you also create custom objects

When you append properties via Add-Member, you are not creating custom objects. Add-Member adds members to a type without changing the type. The PowerShell type system treats such objects as if they were of the underlying object type that you started with.

Since custom objects are always unique and never known to PowerShell, the type system can’t help preventing information overflow, and the user always sees all information surfacing in the console.

When this happens, PowerShell uses its default formatting rule: with four or less properties, your objects are formatted as a table, else as a list. This is why many PowerShell scripters try and limit their custom objects to four or less properties to keep the output formatted as table. There are much better ways for this, though, and without the need to restrict information to only four properties.

Let’s take a look at how authors of PowerShell functions can tell the type system the names of default properties so that the type system can work with your custom objects, too.

Adding PSStandardMembers

Whenever PowerShell discovers the secret property PSStandardMembers on an object, it expects to find a PSPropertySet with the names of the default properties.

By adding this property to objects that your function emits, you control the default properties of your returned objects:

function Test-Output
  # create a custom object
  $person = [PSCustomObject]@{
    LastName = 'Weltner'
    FirstName = 'Tobias'
    Gender = 'm'
    Conference = ''
    Country = 'Germany'
    Twitter = 'tobiaspsp'

  # create a PSPropertySet with the default property names
  [string[]]$visible = 'LastName','Country','Twitter'
  [Management.Automation.PSMemberInfo[]]$visibleProperties = [System.Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet',$visible)
  # before outputting, append the PSStandardMembers property:
  $person |
  Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $visibleProperties -PassThru

When you run Test-Output, only the properties LastName, Country, and Twitter are returned, and since these are less than five, they are formatted as table:

LastName Country Twitter  
-------- ------- -------  
Weltner  Germany tobiaspsp

In reality, Test-Output returned an object that contains more information. Use Select-Object to choose which properties to display:

Test-Output | Select-Object -Property *
LastName   : Weltner
FirstName  : Tobias
Gender     : m
Conference :
Country    : Germany
Twitter    : tobiaspsp

This approach is best when you ship stand-alone functions because everything required happens inside of your function, and there are no prerequisites to consider.

However, with this approach you are cheating a little bit because you are submitting the list of default properties per object and not per type. Theoretically, you could choose different default properties per object, even if they were of same type - no good for consistency. In addition, if you ever chose to pick different default properties, you’d have to touch all of your code that added the information to objects. You can’t centrally manage this approach.

Adding PSTypeNames And Updating PowerShell

PowerShell typically handles formatting per type, so the best approach is to add information about your type to the internal PowerShell type system once and leave the rest to PowerShell.

Any customization of the PowerShell type system is per session, so once you close PowerShell, all customizations are lost and need to be re-imported the next time you launch PowerShell. More about this in a moment.

Which raises the question: what is the type of custom objects you create? And the answer is: whatever you want! Assign any custom type! You can make up your own. Just make sure it is unique.

1. Add Your Own Custom Type

All objects have a secret property called PSCustomTypes which is a list. Use its method Add() to add new types. You can name them any way you want (as long as they don’t collide with existing types):

function Test-Output
  # create a custom object
  $person = [PSCustomObject]@{
    LastName = 'Weltner'
    FirstName = 'Tobias'
    Gender = 'm'
    Conference = ''
    Country = 'Germany'
    Twitter = 'tobiaspsp'

  # assign your own custom type to your custom object:
  # return object

When you run Test-Output, you’ll notice that still all properties show. When you look at the type names, you’ll see why:

$result = Test-Output

The list of types does not match any type in PowerShells internal hint lists (yet):


2. Add Your Custom Type To PowerShells Hint Lists

So in a second step, you need to tell PowerShell what the default types for your new type psconfDelegate are:

Update-TypeData -TypeName psconfDelegate -DefaultDisplayPropertySet 'LastName','Country','Twitter'

Once you run this line and add the information to the type system, Test-Output returns only the listed properties by default:

LastName Country Twitter  
-------- ------- -------  
Weltner  Germany tobiaspsp

Use Select-Object to see all available properties:

Test-Output | Select-Object -Property *
LastName   : Weltner
FirstName  : Tobias
Gender     : m
Conference :
Country    : Germany
Twitter    : tobiaspsp

The great benefit of this approach is that it is type-oriented and centrally managed. If you decide to change the default properties one day, all you need to do is update the information in PowerShells type system using a simple call to Update-TypeData.

On the other hand, this approach requires that you do call Update-TypeData, and if you are shipping a stand-alone function, the previous approach is much better suited.

In practice, this approach works well if you ship your functions inside of PowerShell modules: simply add Update-TypeData to your module code so it gets executed when the module is imported. This way, the PowerShell type system is automatically updated when your functions run.

Use ps1xml Files

The most flexible and professional way to import custom type information into PowerShells type system is to use a stand-alone .ps1xml file.

With this file, you can define the default properties plus the formatting of these properties. So you can also define the width of table columns as well as their alignment.

Designing .*.ps1xml File

In our example, the file would look like this:

<?xml version="1.0" encoding="utf-8" ?>

ViewSelectedBy lists the type(s) that this format applies to. TableHeaders defines the formatting of table columns, and TableRowEntries defines the properties to show by default. Obviously, the number of TableHeaders must match the number of TableRowEntries.

PowerShell uses a lot of .format.ps1xml and .types.ps1xml files to drive its type system. You can see these here (only look, do not touch, or bad things may happen):

Get-ChildItem -Path $PSHOME -Filter *.ps1xml -Recurse

Formatting information like the ones in our own xml above go into .format.ps1xml files and are imported into the type system via Update-FormatData. Type information such as adding properties and methods is done via .types.ps1xml files. These are imported via Update-TypeData.

There is some overlap between formatting and type extensions. In the previous example I used Update-TypeDatato define default properties for types. With .format.ps1xml files, you need to use Update-FormatData instead.

Loading *.format.ps1xml File

Save above file with the file extension .format.ps1xml. You can now manually load it into PowerShells type system:

Update-FormatData -PrependPath c:\...\yourfile.format.ps1xml

By using the parameter -PrependPath you are adding the new formatting information at the top and overwrite (shadow) potentially existing formatting information.

If you use the parameter -AppendPath, you are adding your new information at the bottom of the pile instead.

Once done, run Test-Output again. The output formatting immediately changed:

LastName   Country           Twitter
--------   -------           -------
Weltner    Germany         tobiaspsp

The column width is now defined per column, and the column Twitter is wider and uses right-alignment.

AutoLoading Via Module Manifest

Typically, .ps1xml files are used inside PowerShell modules, and it’s really simple:

  • Make sure your functions add their custom types to their output objects
  • Add a .ps1xml file per custom type to your module

In modules, the preferred way of loading .ps1xml files is via the module manifest. The manifest is a .psd1 file with a number of key-value pairs. The key FormatsToProcess is an array of relative paths to the .ps1xml files in your module.

Let’s assume you created a subfolder named Formats in your module folder. Then in your manifest, you’d add this key: FormatsToProcess = @("Formats/type1.ps1xml", "Formats/anothertype.ps1xml", "Formats/onemore.ps1xml")

What’s Next

Make sure you have PowerShell Conference EU 2020 on your radar. That’s the place to be in June if you enjoy stuff like this. See you there!