Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Better PowerShell Properties

Posted on June 2, 2022June 2, 2022

I was chatting with my friend Gladys Kravitz recently about some PowerShell scripting she was doing with Active Directory and the DirectorySearcher object. For a number of reasons, the Active Directory module from Remote Server Administration Tools (RSAT) is not an option. But that's fine. Gladys is an experience AD admin and PowerShell scripter. Still, she had a question about the output from her commands, which led to some interesting work that you might find valuable. Even though I'm going to be discussing a specific object type, there are several principles that you can apply to your PowerShell scripting.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

Search Result Properties

Let's begin with some PowerShell code to find a user account using the DirectorySearcher object.

$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.filter ="(&(objectcategory=person)(objectclass=user))"
$searcher.FindOne().Properties

The DirectorySearcher has methods of FindOne() and FindAll(). The resulting object has a Properties property.

Active Directory search result properties

You can use this in any number of ways.

New-Object -typename PSObject -property $searcher.FindOne().Properties | Select Name,Description,WhenChanged
new object from result properties

Leaving the formatting of the values aside, notice the property names. Functional, but not professional-looking. This is always an option.

New-Object -typename PSObject -property $searcher.FindOne().Properties | 
Select-Object @{Name="Name";Expression = {$_.name}},
@{Name="Description";Expression = {$_.Description}},
@{Name="WhenChanged";Expression = {$_.WhenChanged}}

Or you can define a custom object with a type name and use a custom format file. But that's a lot of work and not flexible.

Here's another approach to creating an object but with properly cased property names.

$r = $searcher.FindOne()
$r.Properties.GetEnumerator() | 
Where-Object { $_.key -match "Name|Description|WhenChanged|memberof|logoncount" } | 
ForEach-Object -Begin {
    #initialize an empty hashtable
    $h = @{}
} -Process {
    #convert to title case
    $n = [cultureinfo]::CurrentCulture.TextInfo.ToTitleCase($_.name.tolower())
    if ($_.value.count -gt 1) {
        #get the array of values
        $v = $_.value
    }
    else {
        #get the single value
        $v = $_.value[0]
    }
    #add the property name and value to the hashtable
    $h.Add($n, $v)
} -End { 
    #create a custom object from the hashtable
    [pscustomobject]$h 
}

In this particular situation, the object I am formatting is a DirectoryServices.ResultCollection, which is, in essence, a hashtable, so it requires a different approach. The GetEnumerator() method writes each element in the collection as an object with Key and Value properties. My example is filtering keys for specific "properties."

The filtered objects are then piped to ForEach-Object. Before any objects are processed in the pipeline, I initialize an empty hashtable. Next, for each filtered result, I convert the name, which is an alias for 'key,' to title case. For this to work, I always make the string all lower case before invoking the method. This reformatted name and the corresponding value are added to the hashtable. After everything has been processed, I turn the hashtable into a custom object.

custom search result with proper title case

This is promising but needs to be more flexible. The ForEach-Object structure is the same format as an advanced PowerShell function that takes pipeline input. In other words, I have a prototype.

Creating Configuration Data

My idea is to take a DirectorySearcher result and create a custom object replacing the property name with a properly formatted name and the value. But in looking at the output, I want more than title case. 'Logoncount' might look nicer formatted as 'LogonCount.' I prefer SamAccountName. I want to replace the incoming property name with a properly formatted name. I will need a list of all possible property names formatted the way I want. Because I know I will eventually want to use this list as a hashtable, I'll create a configuration data file that I can use later with Import-PowerShellDataFile.

Here's the PowerShell script to generate different configuration data files depending on the object type.

#requires -version 5.1

<# Create-ADPropertyFile.ps1
 proof-of-concept code to create an Active Directory property configuration data file
 .\Create-ADPropertyFile.ps1 -Filter "(&(objectclass=organizationalunit)(Name=Employees))" -FilePath .\adou.psd1
 .\Create-ADPropertyFile.ps1 Group -FilePath .\adgroup.psd1
#>

[cmdletbinding(SupportsShouldProcess, DefaultParameterSetName = "byType")]
Param(
    [Parameter(Position = 0, ParameterSetName = "byType")]
    [ValidateSet("User", "Group", "Computer", "OU")]
    [string]$ObjectType = "User",

    [Parameter(HelpMessage = "Specify an LDAP search filter to a template object.", ParameterSetName = "byFilter")]
    [ValidateNotNullOrEmpty()]
    [string]$Filter,

    [Parameter(Mandatory, HelpMessage = "Specify the filename and path to the psd1 file.")]
    [ValidateScript({ Test-Path (Split-Path $_) })]
    [string]$FilePath
)

if ($pscmdlet.ParameterSetName -eq "byType") {

    Switch ($ObjectType) {
        "User" { $filter = "(&(objectcategory=person)(objectclass=user))" }
        "Group" { $filter = "(Objectclass=group)" }
        "Computer" { $filter = "(objectclass=computer)" }
        "OU" { $filter = "(objectclass=organizationalunit)" }
    }
}

$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.filter = $filter
#don't need values from the search
$searcher.PropertyNamesOnly = $true
$searcher.FindOne().Properties.GetEnumerator() | Sort-Object -Property Key |
ForEach-Object -Begin {
    #initialize a list
    $list = [System.Collections.Generic.List[string]]::new()
    $list.add("@{")
} -Process {
    $value = [cultureinfo]::CurrentCulture.TextInfo.ToTitleCase($($_.key.ToLower()))
    #add each property to the list
    $List.Add("$($_.key) = '$value'")
} -End {
    #close the psd1
    $list.Add("}")
}
#save the list to a file
$list | Out-File -FilePath $FilePath

#edit psd1 file with your desired formatting

The default filters will find the first matching result. However, the DirectorySearcher will only return property names that have been defined. You might want to create a template object with all the properties defined that you intend to use and then build the property file using a custom LDAP filter to that object.

I'll use the script to create a psd1 file for user objects.

c:\scripts\Create-ADPropertyFile.ps1 -ObjectType User -FilePath c:\scripts\aduser.psd1

I can edit the file and adjust the property names.

@{
    accountexpires         = 'AccountExpires'
    admincount             = 'AdminCount'
    adspath                = 'AdsPath'
    badpasswordtime        = 'BadPasswordTime'
    badpwdcount            = 'BadPwdCount'
    cn                     = 'CN'
    codepage               = 'CodePage'
    countrycode            = 'CountryCode'
    department             = "Department"
    description            = 'Description'
    displayname            = "DisplayName"
    distinguishedname      = 'DistinguishedName'
    dscorepropagationdata  = 'DscorePropagationData'
    givenname              = "GivenName"
    instancetype           = 'InstanceType'
    iscriticalsystemobject = 'IsCriticalSystemObject'
    lastlogoff             = 'LastLogoff'
    lastlogon              = 'Lastlogon'
    lastlogontimestamp     = 'LastLogonTimestamp'
    logoncount             = 'LogonCount'
    logonhours             = 'LogonHours'
    memberof               = 'MemberOf'
    name                   = 'Name'
    objectcategory         = 'ObjectCategory'
    objectclass            = 'ObjectClass'
    objectguid             = 'ObjectGuid'
    objectsid              = 'ObjectSid'
    primarygroupid         = 'PrimaryGroupId'
    pwdlastset             = 'PwdlastSet'
    samaccountname         = 'SamAccountName'
    samaccounttype         = 'SamAccountType'
    sn                     = "Surname"
    title                  = "Title"
    useraccountcontrol     = 'UserAccountcontrol'
    usnchanged             = 'UsnChanged'
    usncreated             = 'UsnCreated'
    whenchanged            = 'WhenChanged'
    whencreated            = 'WhenCreated'
}

}

If you know additional LDAP property names, you could manually add them. The last step is to use this file.

Optimize AD Search Result

We tend to avoid monolithic commands in PowerShell. Instead, we want to leverage the pipeline. I have code using the DirectorySearcher to find objects.

directory searcher results

I want to take these results and optimize the property names.

Function Optimize-ADSearchResult {
    [cmdletbinding()]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            HelpMessage = "This should be the input from an ADSearcher FindOne() or FindAll() method."
        )]
        [System.DirectoryServices.SearchResult]$InputObject,

        [Parameter(
            Mandatory,
            HelpMessage = "Enter the path to the psd1 file with your property names. See Create-ADPropertyFile."
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ })]
        [ValidatePattern('\.psd1$')]
        [string]$ConfigurationData,

        [Parameter(HelpMessage = "Specify a custom type name, like CorpADUser. You might add this if using a custom format file or type extensions.")]
        [ValidateNotNullOrEmpty()]
        [string]$TypeName
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
        #import the configuration data
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Importing configuration data from $(Convert-Path $ConfigurationData)"
        $PropertyData = Import-PowerShellDataFile -Path $ConfigurationData
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing input"

        $InputObject.properties.GetEnumerator() |
        ForEach-Object -Begin {
            $new = [ordered]@{ }
            if ($TypeName) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using typename $TypeName"
                $new.Add("PSTypename", $TypeName)
            }
        } -Process {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing property $($_.key)"
            #Get formatted property name from configuration data
            if ($PropertyData.Contains($_.key)) {
                $name = $PropertyData["$($_.key)"]
            }
            else {
                $name = $_.key
            }
            if ($_.value.count -gt 1) {
                $value = $_.value
            }
            else {
                $value = $_.value[0]
            }
            $new.Add($name, $value)
        } -End {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating output"
            New-Object -TypeName psobject -Property $new
        }
    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #end
}

The function looks at each search result and tries to match the property name from the configuration data. If it finds a match, it will be used. Otherwise, PowerShell will use the original property name.

I could have stored all of the property name hashtables in the function, but that would have added complexity and unnecessary length. If I need to add a property, I have to edit the function. It is much better to separate data from the code that implements it. My function is a good example of using configuration data.

I can take the search results, optimize them with this function, and then select what I need.

 $searcher = New-Object System.DirectoryServices.DirectorySearcher
 $searcher.filter = "(&(objectcategory=person)(objectclass=user)(department=sales))"
 $searcher.FindAll() | 
 Optimize-ADSearchResult -ConfigurationData C:\scripts\aduser.psd1 |
 Select-Object -property DistinguishedName,Givenname,Surname,Description,Title
property optimized search results

Summary

There is a lot going on in this post—many moving parts and possibly some new commands. I hope you'll review the code and at least walk through it in your head. Please feel free to leave comments and questions.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

1 thought on “Better PowerShell Properties”

  1. Pingback: Better PowerShell Properties - The Lonely Administrator - Syndicated Blogs - IDERA Community

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d