Skip to content

Instantly share code, notes, and snippets.

@JustinGrote
Created February 2, 2022 02:46
Show Gist options
  • Save JustinGrote/5b7893b4e84bee3c94754e85fc35526f to your computer and use it in GitHub Desktop.
Save JustinGrote/5b7893b4e84bee3c94754e85fc35526f to your computer and use it in GitHub Desktop.
A demonstration of using Write-Progress in a thread-safe way inside Foreach-Parallel
using namespace System.Collections.Concurrent
using namespace System.Collections.Generic
function Get-GroupCompareInfo {
[OutputType([SortedDictionary[string, string[]]])]
<#
.SYNOPSIS
Creates a simple dictionary useful for comparing groups across tenants
.DESCRIPTION
This boils down a list of groups supplied by GroupID to a dictionary containing the unique alias of the group and the left-side UPN of the users
This is useful when comparing to another directory to make sure the groups are the same
#>
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)][Guid]$GroupId
)
begin {
#We collect the group Ids so we can process them in parallel
[Hashset[Guid]]$groupIds = @()
}
process {
$isUnique = $groupIds.Add($GroupId)
if (-not $isUnique) { Write-Warning "$groupId is a duplicate entry, skipping..." }
}
end {
#Track progress
[int]$progressIndicator = 0
[int]$progressId = Get-Random
$progressIndicatorRef = [ref]::new($progressIndicator)
#We have to do a lot of lookups of members, this will allow for a simple deduplication of those members to improve performance
$memberCache = [ConcurrentDictionary[guid, string]]::new()
#A simple table with the mail alias of the group and the shortname of the members. This is for easy comparison with the other tenant
$groupMembers = [ConcurrentDictionary[string, string[]]]::new()
Write-Progress -Id $progressId -Activity 'Gathering Group Comparison Info'
$groupIds | ForEach-Object -Verbose -Throttle 10 -Parallel {
param (
$memberCache = $USING:memberCache,
$groupMembers = $USING:groupMembers,
$parentProgressId = $USING:progressId,
$progressIndicatorRef = $USING:progressIndicatorRef,
$groupIdCount = $USING:groupIds.Count
)
#Optimize load time by only importing the functions we need
Import-Module Microsoft.Graph.Users, Microsoft.Graph.Groups -Function 'Get-MgUser', 'Get-MgGroup', 'Get-MgGroupMember'
$progressParams = @{
Id = (Get-Random)
ParentId = $parentProgressId
Activity = 'Building Membership Table: ' + $PSItem
}
try {
$DebugPreference = 'continue'
[Guid]$groupId = $PSItem
Write-Progress @progressParams -Status 'Getting Group Name'
[String]$groupName = (Get-MgGroup -GroupId $groupId -Property mailNickname).mailNickname
$progressParams.Activity = "Building Membership Table: $groupName ($PSItem)"
[Guid[]]$memberIds = (Get-MgGroupMember -GroupId $groupId).id
$i = 0
$groupMembers[$groupName] = $memberIds | ForEach-Object {
Write-Progress @progressParams -Status "Resolving $PSItem to Username" -PercentComplete ($i / $memberIds.count * 100)
$cached = $memberCache.ContainsKey($PSItem)
Write-Debug "$PSItem Cache $($cached ? 'Hit' : 'Miss')"
$userResult = if ($cached) {
Write-Output $memberCache[$PSItem]
} else {
[string]$username = (Get-MgUser -UserId $PSItem -Property userprincipalname).userprincipalname.split('@')[0]
if (-not $username) { throw "User with GUID $PSItem doesnt exist. This is probably a bug" }
$memberCache[$PSItem] = $username
Write-Output $username
}
$i++
return $userResult
}
} catch { throw } finally {
Write-Progress @progressParams -Completed
#Increment progress in a thread safe way
[void][Threading.Interlocked]::Increment($progressIndicatorRef)
Write-Progress -Id $parentProgressId -Activity 'Gathering Group Comparison Info' -Status "$groupName ($groupId) completed [$($progressIndicatorRef.Value) of $groupIdCount]" -PercentComplete ($progressIndicatorRef.Value / $groupIdCount * 100)
Start-Sleep 10
}
}
Write-Progress -Id $progressId -Activity 'Done' -Completed
return [SortedDictionary[string, string[]]]$groupMembers
}
}
@JustinGrote
Copy link
Author

Demo:

Capture.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment