WQL ASSOCIATORS OF Queries in PowerShell: From WMIC to CimCmdlets

The ASSOCIATORS OF statement in SQL for WMI (WQL) retrieves objects related to a specified source instance, where the source and related objects belong to different classes and are connected through an association class. For example, Win32_LogicalDiskToPartition is an association class that links Win32_DiskPartition and Win32_LogicalDisk. Using this association, you can list the disk partitions related to a specific logical disk (or vice versa). The object paths used to identify the source instances include the DeviceID property, which is intuitive and easy to understand. For instance, DeviceID values are in the form of C: for a logical disk or volume, or Disk #1, Partition #1 for a disk partition, making them more human-readable.

In contrast, the Storage Management Provider (SMP) equivalent classes — MSFT_Partition, MSFT_Volume, and their association class MSFT_PartitionToVolume — do not expose user-friendly object path values. These identifiers are not as intuitive as those in their CIMv2 counterparts. However, a practical workaround is to use the Get-CimAssociatedInstance cmdlet. In this post, I'll demonstrate how this cmdlet simplifies working with such association classes.

1. The Object Path

The object path of a WMI/CIM class instance is a string that serves as its identifier. This path is essential when writing an ASSOCIATORS OF statement in WQL, as it specifies the exact instance for which associated objects are to be retrieved.

In this section, I'll break down the components that make up an object path and demonstrate how to use it to retrieve associated instances of a given class instance. Additionally, I'll compare this method to the approach using the now-deprecated WMIC tool, which previously allowed retrieving associated instances without requiring a properly formatted object path — offering a simpler but now outdated alternative.

1.1 The Key Qualifier

On the official Microsoft Learn page for the ASSOCIATORS OF statement, the examples may be misleading — especially for developers familiar with WMIC, who are now transitioning to PowerShell due to the deprecation of WMIC.

For instance, the query:

ASSOCIATORS OF {Win32_LogicalDisk.DeviceID="C:"}

could incorrectly suggest that other properties like Name or Caption can be used in place of DeviceID since they share the same value. In reality, the object path must be constructed using the key qualifier defined in the class (in this case, DeviceID). Using non-key properties either as identifiers or as general filters in the ASSOCIATORS OF clause will result in errors in PowerShell.

# This command returns the associated instances as expected
Get-CimInstance -Query "ASSOCIATORS OF {Win32_LogicalDisk.DeviceID='C:'}"

# These will throw errors because they use non-key properties
Get-CimInstance -Query "ASSOCIATORS OF {Win32_LogicalDisk.Name='C:'}"
Get-CimInstance -Query "ASSOCIATORS OF {Win32_LogicalDisk.VolumeSerialNumber='93C988A5'}"

Assuming that 93C988A5 is the serial number of the C: drive.
For older versions of PowerShell, Get-WmiObject can be used in place of Get-CimInstance.

The working PowerShell command above is functionally equivalent to the following WMIC command:

wmic LogicalDisk WHERE "DeviceID='C:'" ASSOC

LogicalDisk is the alias for Win32_LogicalDisk.

However, unlike PowerShell, WMIC tolerates the use of non-key properties in the WHERE clause of ASSOC queries. The following WMIC commands also work, even though they use properties other than the key qualifier:

wmic LogicalDisk WHERE "Name='C:'" ASSOC
wmic LogicalDisk WHERE "VolumeSerialNumber='93C988A5'" ASSOC

Retrieving the Key Qualifier

To determine which property serves as the key qualifier for a given WMI class, you can inspect the class metadata using either Get-CimClass from the CimCmdlets module for a modern approach, or Get-WmiObject with the switch parameter -List from the Microsoft.PowerShell.Management module for older PowerShell versions.

filter Get-KeyQualifier {
  $_ |
  Select-Object -ExpandProperty $args[0] |
  Where-Object { $_.Qualifiers.Name -contains 'key' } |
  Select-Object -ExpandProperty Name
}

# Using Get-WmiObject
Get-WmiObject Win32_LogicalDisk -List |
Get-KeyQualifier Properties

# Using Get-CimClass
Get-CimClass Win32_LogicalDisk |
Get-KeyQualifier CimClassProperties

Both commands return:

DeviceID

Multiple Key Qualifiers

A WMI class can define composite key qualifiers, meaning multiple properties are required to uniquely identify an instance.

For example, the Win32_UserAccount class uses both the Domain and Name properties as key qualifiers:

PS > Get-CimClass Win32_UserAccount |
Get-KeyQualifier CimClassProperties
Domain
Name

When constructing a WMI object path for such classes, both key properties must be included. The keys order does not matter.

For example, the two queries below are functionally equivalent and both retrieve instances associated with the built-in Guest account on the current host:

Get-CimInstance -Query "ASSOCIATORS OF {Win32_UserAccount.Name='Guest',Domain='$(hostname)'}"
Get-CimInstance -Query "ASSOCIATORS OF {Win32_UserAccount.Domain='$(hostname)',Name='Guest'}"

For WMI classes with composite keys, omitting the key property names in the object path is invalid and confusing. By contrast, for classes with a single key qualifier, omitting the key name is allowed.

For example:

Get-CimInstance -Query "ASSOCIATORS OF {Win32_LogicalDisk='C:'}"

1.2 The Full Object Path

An important detail to understand when working with WMI/CIM queries is to identify the full object path. The following PowerShell and WMIC commands:

wmic "/NODE:'$Env:COMPUTERNAME'" /NAMESPACE:\\ROOT\CIMV2 PATH Win32_LogicalDisk WHERE "DeviceID='C:'" ASSOC

Get-CimInstance -ComputerName $Env:COMPUTERNAME -Namespace ROOT\CIMV2 -Query "ASSOCIATORS OF {\\$Env:COMPUTERNAME\ROOT\CIMV2:Win32_LogicalDisk.DeviceID='C:'}"

Get-CimInstance -ComputerName $Env:COMPUTERNAME -Namespace root\Microsoft\Windows\Storage -Query "ASSOCIATORS OF {\\$Env:COMPUTERNAME\root\Microsoft\Windows\Storage:MSFT_Volume.ObjectId=`"$volumeObjectId`"}"

The variable $volumeObjectId is computed using methods described in later sections.

are equivalent to:

wmic PATH Win32_LogicalDisk WHERE "DeviceID='C:'" ASSOC

Get-CimInstance -Query "ASSOCIATORS OF {Win32_LogicalDisk='C:'}"

Get-CimInstance -Namespace root\Microsoft\Windows\Storage -Query "ASSOCIATORS OF {MSFT_Volume=`"$volumeObjectId`"}"

The full path can often be shortened because the default WMI/CIM namespace is \\root\cimv2 (the namespace specified in the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WBEM\Scripting\Default Namespace) and the local computer is used by default if no hostname is specified. the name of the local computer can be retrieved via the hostname.exe command-line tool or the COMPUTERNAME environment variable. When working with remote systems, you can replace it with the appropriate machine name.

The string \\$Env:COMPUTERNAME\ROOT\CIMV2:Win32_LogicalDisk.DeviceID="C:" represents the full object path of the specified instance of Win32_LogicalDisk.

The Object Path Format

The general structure of a WMI object path is:

\\<host name>\<namespace>\<class name>.<key qualifier>=<value>

The path <namespace>:<class name> is also referred to as the full class name.

The <Value> must be properly escaped, especially if it contains special characters such as backslashes (\) or quotation marks ("). Use a backslash (\) as the escape character within the string.

Retrieving the Full Object Path

Here are a few ways to retrieve the full object path for a WMI class instance — for example, Win32_LogicalDisk, assuming the current host is named MyHost:

# Using WMIC
function Get-WmiObjectPath {
  end {
    $input |
    Where-Object { $_.StartsWith('__PATH') } |
    Select-Object -First 1 |
    ForEach-Object { $_ -split '=',2 } |
    Select-Object -Last 1
  }
}

wmic PATH Win32_LogicalDisk WHERE "DeviceID='C:'" ASSOC:List |
Get-WmiObjectPath

# Using Get-WmiObject
Get-WmiObject -Query 'SELECT __PATH FROM  Win32_LogicalDisk WHERE DeviceID="C:"' |
Select-Object -ExpandProperty __PATH

Get-WmiObject is not available in PowerShell Core, though it remains useful for quickly accessing metadata like __PATH.

Both methods return:

\\MyHost\ROOT\CIMV2:Win32_LogicalDisk.DeviceID="C:"

WMIC vs. Get-WmiObject: A Caution with Encoded Entities

When using wmic to retrieve object paths, be aware that special characters like ampersands (&) may be encoded as XML entities (&amp;). This can cause issues when the path is reused in a WQL query without decoding.

For example, consider the MSFT_Partition class (SMP equivalent of Win32_DiskPartition). Its object path might contain &amp;, which will break a WQL query:

PS > $ErrorView = 'CategoryView'
PS > $objectPath1 = wmic /NAMESPACE:\\root\Microsoft\Windows\Storage PATH MSFT_Partition WHERE "DriveLetter='C'" ASSOC:List |
Get-WmiObjectPath
PS > Get-WmiObject -Query "ASSOCIATORS OF {$objectPath1}" -Namespace root\Microsoft\Windows\Storage
InvalidOperation: (:) [Get-WmiObject], ManagementException

The root cause:

PS > $objectPath.Contains('&amp;')
True

Whereas, retrieving the object path using Get-WmiObject avoids this problem:

PS > $objectPath2 = Get-WmiObject MSFT_Partition -Namespace root\Microsoft\Windows\Storage -Filter "DriveLetter='C'" -Property __PATH |
Select-Object -ExpandProperty __PATH
PS > Get-WmiObject -Query "ASSOCIATORS OF {$objectPath2}" -Namespace root\Microsoft\Windows\Storage |
Select-Object -ExpandProperty __CLASS
MSFT_StorageSubSystem
MSFT_Disk
MSFT_Volume

You can verify the paths are the same after decoding:

PS > $objectPath1.Replace('&amp;', '&') -eq $objectPath2
True

Using Get-CimInstance (manually building the object path):

Because Get-CimInstance does not expose the __PATH property directly, you must construct the object path yourself. This makes Get-CimInstance slightly more cumbersome than Get-WmiObject, which exposes the __PATH property directly. However, Get-CimInstance is the recommended cmdlet to use, as Get-WmiObject is not available in modern PowerShell environments.

This Get-ObjectPath filter uses the Get-KeyQualifier defined earlier to dynamically build the full object path for a CimInstance:

AugmentedGetCimInstance.ps1
filter Get-ObjectPath {
  $object = $_
  $namespace,$classname = $object.CimClass -split ':'
  $keyQualifiers = Get-CimClass $classname -Namespace $namespace |
    Get-KeyQualifier CimClassProperties |
    ForEach-Object {
      $keyValue = $object.$_ -replace '\\','\\' -replace '"','\"'
      "$_=`"$keyValue`""
    }
  $keyQualifiers = $keyQualifiers -join ','
  $fullclassname = $object.CimClass -replace '/','\'
  return "\\$Env:COMPUTERNAME\$fullclassname.$keyQualifiers"
}

The following version of Get-CimInstance wraps the original CimCmdlets\Get-CimInstance and augments each returned object with a computed __PATH property:

AugmentedGetCimInstance.ps1
function Get-CimInstance {
  [CmdletBinding(DefaultParameterSetName = 'ClassName')]
  param(
    [Parameter(ParameterSetName = 'Query', Position = 0)]
    [string]$Query,
    [Parameter(ParameterSetName = 'ClassName', Position = 0)]
    [string]$ClassName,
    [string]$Namespace = (Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\WBEM\Scripting" -Name "Default Namespace"),
    [string]$ComputerName = $Env:COMPUTERNAME,
    [string]$Filter,
    [string[]]$Property,
    [switch]$KeyOnly
  )
  $instances = CimCmdlets\Get-CimInstance @PSBoundParameters
  foreach ($obj in $instances) {
    # Augment each object (add __PATH note property)
    $obj |
    Add-Member -NotePropertyName '__PATH' -NotePropertyValue (
      $obj | Get-ObjectPath
    ) -Force
  }
  return $instances
}

Example usage:

PS > . .\AugmentedGetCimInstance.ps1

# Get associated instances to the current process object
PS > $object = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID"
PS > Get-CimInstance -Query "ASSOCIATORS OF {$($object.__PATH)} WHERE ClassDefsOnly" |
Select -ExpandProperty CimClass
ROOT/CIMV2:Win32_ComputerSystem
ROOT/cimv2:Win32_LogonSession
ROOT/cimv2:CIM_DataFile

# Get associated instances to the Guest user account object
PS > $object = Get-CimInstance -ClassName Win32_UserAccount -Filter "Name='Guest'"
PS > Get-CimInstance -Query "ASSOCIATORS OF {$($object.__PATH)} WHERE ClassDefsOnly" |
Select -ExpandProperty CimClass
ROOT/CIMV2:Win32_ComputerSystem
ROOT/CIMV2:Win32_Desktop
ROOT/cimv2:Win32_SID
ROOT/CIMV2:Win32_Group

# Get associated instances to the C: drive object
PS > $object =  Get-CimInstance -Namespace root\Microsoft\Windows\Storage -ClassName MSFT_Volume -Filter "DriveLetter='C'"
PS > Get-CimInstance -Query "ASSOCIATORS OF {$($object.__PATH)} WHERE ClassDefsOnly" -Namespace root\Microsoft\Windows\Storage |
Select -ExpandProperty CimClass
ROOT/Microsoft/Windows/Storage:MSFT_StorageNode
ROOT/Microsoft/Windows/Storage:MSFT_StorageSubSystem
ROOT/Microsoft/Windows/Storage:MSFT_FileShare
ROOT/Microsoft/Windows/Storage:MSFT_Partition
ROOT/Microsoft/Windows/Storage:MSFT_FileServer

2. WMIC vs Get-CimAssociatedInstance

In this section, I will demonstrate how to use Get-CimAssociatedInstance to retrieve associated instances of a source WMI instance without needing to reference its key qualifier directly — similar to how WMIC operates.

When dealing with opaque or inconvenient key qualifiers, we've fallen back to WMIC, which allows querying using more readable properties, as mentioned in the previous section.

To illustrate this, consider the ObjectId property of an MSFT_Volume instance. Although it uniquely identifies the instance, its value is not user-friendly. Fortunately, MSFT_Volume also exposes a more intuitive property: DriveLetter. This single-character value (e.g., C) corresponds to the actual drive letter assigned to the volume and is unique — similar to Win32_LogicalDisk.

To find the associated MSFT_Partition for the volume with drive letter C, you can use the following approach with WMIC:

PS > $associatedInstance = wmic /NAMESPACE:\\root\Microsoft\Windows\Storage PATH MSFT_Volume WHERE "DriveLetter='C'" ASSOC:List /RESULTCLASS:MSFT_Partition
PS > $associatedInstance = $associatedInstance -match '^(PartitionNumber)|(DiskNumber)|(__CLASS)' -join [Environment]::NewLine
PS > $associatedInstance = ConvertFrom-StringData $associatedInstance
PS > [pscustomobject] $associatedInstance

DiskNumber PartitionNumber __CLASS
---------- --------------- -------
0          3               MSFT_Partition

As demonstrated, parsing WMIC output is not ideal since the result is just a string dump. We use ConvertFrom-StringData (or similar workarounds) to convert the output into a usable object.

A cleaner and more PowerShell-native approach is to use the Get-CimAssociatedInstance cmdlet in combination with Get-CimInstance.

PS > Get-CimInstance MSFT_Volume -Namespace root\Microsoft\Windows\Storage -Filter "DriveLetter='C'" |
Get-CimAssociatedInstance -ResultClassName MSFT_Partition |
Select-Object -Property DiskNumber,PartitionNumber,CimClass

DiskNumber PartitionNumber CimClass
---------- --------------- --------
         0               3 root/Microsoft/Windows/Storage:MSFT_Partition

This method is functionally equivalent to the WMIC approach but returns full PowerShell objects, making it the preferred method in modern scripting — especially considering WMIC's deprecation.

3. Application to the Custom Drive Letter Assigner

In the Custom Drive Letter Assigner script, it was necessary to identify the disk partition associated with a volume, and vice versa. To handle this, I developed two functions:

  • :getPartitionName - returns the partition name (e.g., Disk #0, Partition #2) for a given drive letter.
  • :getDriveLetter - performs the reverse operation, retrieving the drive letter for a given partition.

Both functions are implemented in the two scripts I created for the project: swap.bat, where I experimented with CIMv2 WMI classes, and assign.bat, the main script, which uses the more modern SMP classes (underlying the PowerShell Storage module).

swap.bat

As explained in this post, I used two straightforward ASSOCIATORS OF WQL statements to establish the relationship between disk partitions and drive letters. The key qualifier DeviceID is defined for both the Win32_DiskPartition (which provides the partition name) and the Win32_LogicalDisk (which provides the drive letter).

-- Used in :getPartitionName
ASSOCIATORS OF {Win32_LogicalDisk="<drive letter>"} WHERE ResultClass=Win32_DiskPartition

-- Used in :getDriveLetter
ASSOCIATORS OF {Win32_DiskPartition="<disk partition name>"} WHERE ResultClass=Win32_LogicalDisk

In the :getPartitionName function, the drive letter is passed as the first argument. The corresponding WMIC command embedded in the batch script is:

wmic LogicalDisk WHERE DeviceID="%~1" ASSOC:List /RESULTCLASS:Win32_DiskPartition 2> nul | findstr /b "Name="

I later transitioned this logic into an embedded PowerShell command. The PowerShell equivalent (where $args[0] is the drive letter) is:

Get-CimInstance -Query "ASSOCIATORS OF {Win32_LogicalDisk='$($args[0])'} WHERE ResultClass=Win32_DiskPartition" |
Select-Object -ExpandProperty Name

For the :getDriveLetter function, the commands are analogous — the class names in the ASSOCIATORS OF queries are simply swapped to reverse the lookup.

assign.bat

As explained in this post, I eventually adopted Get-CimAssociatedInstance to return the partition information from an MSFT_Volume instance. It's essentially a “hack” of the following query by replacing the <unused object id> with the assigned volume drive letter, accessed via the DriveLetter property:

-- Used in :getPartitionName
ASSOCIATORS OF {\\MyHost\root\Microsoft\Windows\Storage:MSFT_Volume="<unused object id>"} WHERE ResultClass=MSFT_Partition

The PowerShell equivalent later embedded in the batch script is:

Get-CimInstance MSFT_Volume -Namespace root\Microsoft\Windows\Storage -Filter "DriveLetter='$($args[0])'" -Property ObjectId |
Get-CimAssociatedInstance -ResultClassName MSFT_Partition |
Format-Table DiskNumber,PartitionNumber

I particularly liked this solution because embedding this PowerShell command in a for /f loop within a batch file is much simpler than using the ObjectId key.

# Using Get-WmiObject
Get-WmiObject MSFT_Volume -Namespace root\Microsoft\Windows\Storage -Filter "DriveLetter='$($args[0])'" -Property __PATH |
Select-Object -ExpandProperty __PATH |
ForEach-Object {
    Get-WmiObject -Query "ASSOCIATORS OF {$_} WHERE ResultClass=MSFT_Partition" -Namespace root\Microsoft\Windows\Storage
} |
Format-Table DiskNumber,PartitionNumber

# Using Get-CimInstance (even more complex escaping)
Get-CimInstance MSFT_Volume -Namespace root\Microsoft\Windows\Storage -Filter "DriveLetter='$($args[0])'" -Property ObjectId |
ForEach-Object {
    $_.ObjectId -replace '\\','\\' -replace '"','\"'
} |
ForEach-Object {
    Get-CimInstance -Query ('ASSOCIATORS OF {{MSFT_Volume="{0}"}} WHERE ResultClass=MSFT_Partition' -f $_) -Namespace root\Microsoft\Windows\Storage
} |
Format-Table DiskNumber,PartitionNumber

To be exhaustive, here is the WMIC equivalent of the embedded PowerShell command:

wmic /NAMESPACE:\\root\Microsoft\Windows\Storage PATH MSFT_Volume WHERE DriveLetter="%~1" ASSOC:List /RESULTCLASS:MSFT_Partition 2> nul | findstr /b "DiskNumber= PartitionNumber="

Ultimately, this exploration was more of an experiment. In fact, a simple SELECT query on the MSFT_Partition class is sufficient to retrieve both the partition name and the drive letter properties, since they're directly exposed:

Get-CimInstance MSFT_Partition -Namespace root\Microsoft\Windows\Storage -Filter "DriveLetter='$($args[0])'" -Property DiskNumber,PartitionNumber |
Format-Table DiskNumber,PartitionNumber

4. Conclusion

In summary, the ASSOCIATORS OF query remains a powerful tool in WMI and CIM for navigating relationships between system components — especially through association classes. While WMIC offered a more forgiving syntax and easier access to object relationships, its deprecation highlights the need to transition to modern alternatives like Get-CimInstance and Get-CimAssociatedInstance. These cmdlets offer robust capabilities, but they require a deeper understanding of object paths, key qualifiers, and namespace conventions. By mastering these concepts—and by leveraging techniques such as programmatically constructing object paths or inspecting class metadata—you can write precise, efficient queries that maintain full compatibility with current and future PowerShell environments.

Comments

Popular posts from this blog

Automate Drive Letter Assignment with a DiskPart Script

Retrieve the Current Command Prompt Process Identifier Using PowerShell

Mutual Exclusion Techniques for Batch Scripts