Forum Widgets
Latest Discussions
Beginners performance tip: Use pipelines.
Hi folks, In my role, I see a lot of poorly-written PowerShell code spanning a lot of different contexts. Without fail, the most common failing I see is that the script simply won't scale, meaning performance will decrease significantly when it's run against larger data sets. And within this context, one of the biggest reasons is the overuse of variables and the underutilisation of the PowerShell pipeline. If you're the investigative type, here's some official documentation and training on the pipeline: about_Pipelines - PowerShell | Microsoft Learn Understand the Windows PowerShell pipeline - Training | Microsoft Learn A short explanation is that piping occurs when the output from one command is automatically sent to and used by another command. As an example, let's say I want my first command to fetch all the files in my temporary directory (just the root in this case). I might run a command like the following: Get-ChildItem -Path $env:TEMP -File Which, as you'd expect, produces a list of files. Where PowerShell differs from the old command prompt (or DOS prompt, if you're old like me) is that it's not simply a bunch of text written to the screen. Instead, each of these files is an object. If you don't know what an object is, think of it as a school lunchbox for the time being, where that lunchbox contains a bunch of useful stuff (just data; nothing tasty, sadly) inside. Because this isn't just a bunch of useless text, we can take the individual lunchboxes (objects) produced from this first command and send those to another command. As the second command sees each lunchbox, it can choose to do something with it - or even just ignore it; the possibilities are endless! When the lunchboxes coming out of the first command travel to the second command, the pathway they travel along is the pipeline! It's what joins the two commands together. Continuing the example above, I now want to remove those lunchboxes - I mean files - from my temporary directory, which I'll do by piping the lunchboxes from the first command into a second command that will perform the deleting. Get-ChildItem -Path $env:TEMP -File | Remove-Item -Confirm:$false -ErrorAction:SilentlyContinue Now, there's no output to show for this command but it does pretty much what you'd expect: Deletes the files. Now, there's another way we could have achieved the same thing using variables and loops, which I'll demonstrate first before circling back to how this relates to performance and scalability. # Get all the files first and assign them to a variable. $MyTempFiles = Get-ChildItem -Path $env:TEMP -File; # Now we have a single variable holding all files, send all those files (objects) to the second command to delete them. $MyTempFiles | Remove-Item -Confirm:$false -ErrorAction:SilentlyContinue; This isn't the only way you you can go about it, and you can see I'm still using piping in the second command - but none of this is important. The important point - and this brings us back to the topic of performance - is that I've assigned all of the files to a variable instead of simply passing them over the pipeline. This means that all of these files consume memory and continue to do so until I get rid of the variable (named $MyTempFiles). Now, imagine that instead of dealing with a few hundred files in my temp directory, I'm dealing with 400,000 user objects from an Azure Active Directory tenant and I'll retrieving all attributes. The difference in memory usage is incomparable. And when Windows starts feeling memory pressure, this impacts disk caching performance and before you know it, the Event Log system starts throwing performance events everywhere. It's not a good outcome. So, the more objects you have, the more your performance decreases in a linear manner. Pipeline to the rescue! This conversation is deliberately simple and doesn't go into the internal mechanics of how many commands you might like using actually work, but only relevant part I want to focus on is something called paging. Let's say you use Get-MgBetaUser to pull down those 400,000 users from Azure Active Directory. Get-MgBetaUser (Microsoft.Graph.Beta.Users) | Microsoft Learn Internally, the command won't be pulling them down all at once. Instead, it will pull down a bite-sized chunk (i.e. a page, as evidenced by the ability to specify a value for the PageSize parameter that features in the above documentation) and push that out onto the pipeline. And if you are piping from Get-MgBetaUser to a second command, then that second command can read that set of users from the pipeline to do what it needs to do. And so on through any other commands until eventually there are no more commands left. At this stage - and this is where the memory efficiency comes in - that batch of users can be released from memory as they are no longer needed by anything. In pictures, and using a page size of 1,000, this looks like: Now, as anyone familiar with .NET can attest to, memory isn't actually released immediately by default. The .NET engine manages memory resourcing and monitoring internally but the key takeaway is by using the pipeline, we're allowing the early release of memory to occur. Conversely, when we store everything in a variable, we're preventing the .NET memory manager from releasing memory early. This, in turn, leads to the above-mentioned performance issues. In pictures, this looks like: Is there real benefit? Yes, absolutely. And it can be quite significant, too. In one case, I triaged a script that was causing system failure (Windows PowerShell has/had a default process limit of 2 GB) through storing Azure results in a variable. After some minor tweaks so that it used the pipeline, process memory fluctuated between 250 MB to 400 MB. Working with pipelines typically doesn't require any extra effort - and in some cases can actually condense your code. However, the performance and scalability benefits can potentially be quite significant - particularly on already-busy systems. Cheers, LainLainRobertsonMar 20, 2025Silver Contributor23Views0likes0CommentsConnecting to multiple Microsoft services with the same session
Hi guys. Working on a script that needs to connect to ExchangeOnlineManagement, TeamsOnlineManagement, SharePointOnlineManagement.... The script will be used across many different tenants, and I also plan to make it publicly available, so 1) I don't really want to pre-configure some complicated key setup and 2) I don't really want to have login pop-ups over and over again... For ExchangeOnline, I learned (accidentally), if I do this: $upn = Read-Host -Prompt "input yer wahawha" Connect-ExchangeOnline -userprimaryname $upn Connect-IPPSsession -userprimaryname $upn And login to MY tenant, I don't get prompted for login. I think likely because my device is Entra-joined, and it's using my Microsoft account. But even if I use a different account, it will only prompt me once - reusing it for the other. This is great, and exactly how I wanted things to flow - but now I'm trying to do Connect-SPOService (sharepoint) and Connect-MicrosoftTeams... and while both of these are part of the tenant, they don't take the -userprimaryname param - so I can specify to use the account I'm logged into my PC with.. The end-goal is to have this script run with minimal user input. I've SORT OF found a workaround for SharePoint, where I can get the SharePointSite from ExchangeOnline, then modify it a bit and use it as input for Connect-SPOService... but Teams, while it doesn't have the URL param requirement, DOES prompt me to login again. Is there a way to use the existing session for either of these, like I've done with ExchangeOnline / IPPSSession? We have MFA enabled, though not required from within our company network - but when I try to use Get-Credential, it errors me out because it wants MFA.66Views1like3CommentsSet historic calendar entries for a specific user to private
Hi all. To improve engagement and to make organising meetings easier, I'd like to automatically set everyone's calendar so that everyone else in the organisation can see the title & location of appointments / meetings. For this I'm going to use the following Exchange Online PowerShell script: Get-Mailbox | ForEach-Object {Set-MailboxFolderPermission $_”:\calendar” -User Default -AccessRights LimitedDetails} However, before I do this, I'd like to offer individuals who have a lot of sensitive meetings (e.g. HR, Finance, Exec's) the option to set all their historic calendar entries to Private (so only invitees would be able to see any details). Is it possible to do this with PowerShell please? It's easy to set their whole calendar to Private, but I'd just like to set historic calendar entries with this tag. Hope you can help, and thank you.OzOscroftMar 15, 2025Iron Contributor1.6KViews1like2CommentsChange work hours
Hello, I am trying to change users' work hours as I would do via the web interface. However, I am unable to find a way to do this using PowerShell. I’ve seen suggestions to use Set-MailboxCalendarConfiguration, such as: Set-MailboxCalendarConfiguration -Identity email address removed for privacy reasons -WorkingHoursStartTime "09:00:00" -WorkingHoursEndTime "17:00:00" However, I need to set different working hours for each day, and I can’t find any parameters that would allow me to do this. Is this possible? Do I need to use Update-MgUserMailboxSetting for this? Thank you, Alejandro40Views0likes2CommentsWhen creating a new team from a template with powershell add new private channel and members
Hi All, I have a powershell script I am using to create and populate new teams from a template and add owners and users via .csv, Everything seem to work fine except the private team in the template is not copied to the new teams. Is there a way to copy the private team with its members from the template? if not how can I add a new private team and add users from a .csv file to my existing script. Import-Module Microsoft.Graph.Teams Connect-MgGraph -Scope Group.ReadWrite.All Connect-MicrosoftTeams $ProgressPreference = 'SilentlyContinue' ######################### #Variable definition: $DefaultModelTeam = "Team template ID" $MembersFilePath = "C:\Users\t130218\Desktop\owlimport_365.csv" $OwnersFilePath = "C:\Users\t130218\Desktop\TeamOwners.csv" ######################### Function CreaTeam{ param( [Parameter(Position=0)] [string]$displayName, [Parameter(Position=1)] [string]$description ) begin{ $params = @{ partsToClone = "apps,tabs,settings,channels" displayName = $displayName description = $description mailNickname = $displayName #visibility = "public" } #Disable "Crea" button in order to avoid duplicate Teams creation $btnCrea.enabled=$false #Message output and waiting time countdown for allow new Tean creation finalization $lblMessaggio.text="Creazione Team in corso..." $teamId= $txtTemplate.text Copy-MgTeam -TeamId $teamId -BodyParameter $params $lblTeamId.text = "Attendere 20 secondi" Start-Sleep -Seconds 5 $lblTeamId.text = "Attendere 15 secondi" Start-Sleep -Seconds 5 $lblTeamId.text = "Attendere 10 secondi" Start-Sleep -Seconds 5 $lblTeamId.text = "Attendere 5 secondi" Start-Sleep -Seconds 5 #The Teamid of the team that was just created can only be discovered via Team name search $newTeam= Get-MgGroup | Where-Object {$_.DisplayName -like $displayName} $lblTeamId.text=$newTeam.Id #Get Team members from the CSV $TeamUsers = Import-Csv $MembersFilePath -delimiter ";" #Iterate through each row obtained from the CSV and add to Teams as a Team member $TeamUsers | ForEach-Object { Add-TeamUser -GroupId $newTeam.id -User $_.m365_email -Role Member Write-host "Added User:"$_.m365_email -f Green } #Get Team owners from the CSV $TeamOwners = Import-Csv $OwnersFilePath -delimiter ";" #Iterate through each row obtained from the CSV and add to Teams as a Team member $TeamOwners | ForEach-Object { Add-TeamUser -GroupId $newTeam.id -User $_.m365_email -Role Owner Write-host "Added Owner:"$_.m365_email -f Green } } } Add-Type -AssemblyName System.Windows.Forms [System.Windows.Forms.Application]::EnableVisualStyles() $CorsoTeams = New-Object system.Windows.Forms.Form $CorsoTeams.ClientSize = New-Object System.Drawing.Point(1200,575) $CorsoTeams.text = "Corso Teams - Crea Struttura" $CorsoTeams.TopMost = $false $lblNomeCorso = New-Object system.Windows.Forms.Label $lblNomeCorso.text = "Nome del corso" $lblNomeCorso.AutoSize = $true $lblNomeCorso.width = 25 $lblNomeCorso.height = 10 $lblNomeCorso.location = New-Object System.Drawing.Point(40,79) $lblNomeCorso.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $btnCrea = New-Object system.Windows.Forms.Button $btnCrea.text = "Crea" $btnCrea.width = 150 $btnCrea.height = 67 $btnCrea.location = New-Object System.Drawing.Point(373,298) $btnCrea.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',16) $btnChiudi = New-Object system.Windows.Forms.Button $btnChiudi.text = "Chiudi" $btnChiudi.width = 150 $btnChiudi.height = 67 $btnChiudi.location = New-Object System.Drawing.Point(628,298) $btnChiudi.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',16) $lblDataCorso = New-Object system.Windows.Forms.Label $lblDataCorso.text = "Data del corso" $lblDataCorso.AutoSize = $true $lblDataCorso.width = 25 $lblDataCorso.height = 10 $lblDataCorso.location = New-Object System.Drawing.Point(39,143) $lblDataCorso.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $lblDescrizione = New-Object system.Windows.Forms.Label $lblDescrizione.text = "Descrizione (facoltativa)" $lblDescrizione.AutoSize = $true $lblDescrizione.width = 25 $lblDescrizione.height = 10 $lblDescrizione.location = New-Object System.Drawing.Point(39,210) $lblDescrizione.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $txtDataCorso = New-Object system.Windows.Forms.TextBox $txtDataCorso.multiline = $false $txtDataCorso.width = 150 $txtDataCorso.height = 40 $txtDataCorso.enabled = $true $txtDataCorso.location = New-Object System.Drawing.Point(370,134) $txtDataCorso.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',20) $txtNomeTeam = New-Object system.Windows.Forms.TextBox $txtNomeTeam.multiline = $false $txtNomeTeam.width = 405 $txtNomeTeam.height = 40 $txtNomeTeam.enabled = $true $txtNomeTeam.location = New-Object System.Drawing.Point(370,75) $txtNomeTeam.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',20) $txtDescrizione = New-Object system.Windows.Forms.TextBox $txtDescrizione.multiline = $false $txtDescrizione.width = 405 $txtDescrizione.height = 40 $txtDescrizione.enabled = $true $txtDescrizione.location = New-Object System.Drawing.Point(370,210) $txtDescrizione.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',20) $btnChiudi = New-Object system.Windows.Forms.Button $btnChiudi.text = "Chiudi" $btnChiudi.width = 150 $btnChiudi.height = 67 $btnChiudi.location = New-Object System.Drawing.Point(628,298) $btnChiudi.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',16) $lblMessaggio = New-Object system.Windows.Forms.Label $lblMessaggio.text = "INSERIRE I DATI" $lblMessaggio.AutoSize = $true $lblMessaggio.width = 25 $lblMessaggio.height = 10 $lblMessaggio.location = New-Object System.Drawing.Point(40,493) $lblMessaggio.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $lblTemplate = New-Object system.Windows.Forms.Label $lblTemplate.text = "Modello Team utilizzato:" $lblTemplate.AutoSize = $true $lblTemplate.width = 25 $lblTemplate.height = 10 $lblTemplate.location = New-Object System.Drawing.Point(40,400) $lblTemplate.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',8) $txtTemplate = New-Object system.Windows.Forms.TextBox $txtTemplate.multiline = $false $txtTemplate.width = 405 $txtTemplate.height = 40 $txtTemplate.enabled = $true $txtTemplate.text = $DefaultModelTeam $txtTemplate.location = New-Object System.Drawing.Point(370,400) $txtTemplate.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',14) $lblTeamId = New-Object system.Windows.Forms.Label $lblTeamId.text = "" $lblTeamId.AutoSize = $true $lblTeamId.width = 25 $lblTeamId.height = 10 $lblTeamId.location = New-Object System.Drawing.Point(540,493) $lblTeamId.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $CorsoTeams.controls.AddRange(@($lblNomeCorso,$btnCrea,$lblDataCorso,$txtDataCorso,$txtNomeTeam,$btnChiudi,$lblMessaggio,$lblDescrizione,$txtDescrizione, $lblTeamId,$lblTemplate,$txtTemplate )) $txtDataCorso.text=Get-Date -Format "dd/MM/yyyy" $btnCrea.Add_Click({ $NomeTeamCompleto=$txtNomeTeam.text+" - "+$txtDataCorso.text CreaTeam $NomeTeamCompleto $txtDescrizione.text $lblMessaggio.text= "Team creato - TeamId:" }) $btnChiudi.Add_Click({$CorsoTeams.Close()}) [void]$CorsoTeams.ShowDialog()phil_tannayMar 14, 2025Copper Contributor23Views0likes1Commentchanging file attribute/metadata
Hello all, when I run a script that gives all of the file properties, I can see one named 'protected' and the value is 'yes'? Now I want this changed to 'No'. I am new to powershell and have been searching everywhere, but did not find any solution for my issue. The script I run to get the file information looks like this: $objShell = New-Object -ComObject Shell.Application $objFolder = $objShell.Namespace((Get-Item .).FullName) $filenameWithExtension = "24 Africa (Single Version).m4a" $objFile = $objFolder.ParseName($filenameWithExtension) $fileMeta = [ordered]@{} for($id = 0; $id -le 266; $id++){ $fileMeta[ $($objFolder.GetDetailsOf($objFolder, $id)) ] = $( if($objFolder.GetDetailsOf($objFile, $id)){ $($objFolder.GetDetailsOf($objFile, $id)) }else{ "" } ) } print ordered hashtable $fileMeta you get a list of properties and one of them is the Protected. Can anyone help on this? thxdyckwalMar 07, 2025Copper Contributor88Views0likes6CommentsA powershell 7 script that will grant a global admin rights to users OneDrive
Hello, I'm wondering if this has ever been done, and if yes, can someone either give me the script or point me to where it is. I have a CSV of users which I need to grant a global admin access to their One Drives. I've scoured the internet for scripts that will iterate through my CSV and grant me access but I am always getting errors no matter what I try to do. I am using PS7 for this. Has this ever been done? If yes can someone please give me a script that can do it. Thanksaudi911111Mar 05, 2025Copper Contributor54Views0likes2CommentsThe term 'New-MailContact' is not recognized
Hi Support, I am trying to bulk import external contacts to the tenant. I have come across several other Q&As which have the similar issue. However, the issue is slightly different - I have assigned this test account recipient management role which should be able to create mail recipient and related stuff. This user, eg, test user, is able to do it from the online version from exchange admin center but cant use the cmd to run via powershell. It was working probably a week ago and I haven't changed anything. It also works fine when authenticating using a global admin account. Looking forward to your reply. Thanks in advance, SheilaSXMar 01, 2025Copper Contributor90Views0likes4CommentsGet-MpComputerStatus returns no output
Hello, on a Server 2019 with windows defender installed in the "Windows Security GUI" all is fine. Protection definitions are up to date, exclusions are set ... (managed with SCCM) But when i use the Get-MpComputerStatus it returns no output. (not even an error) Please help.blindpepperFeb 26, 2025Copper Contributor23KViews2likes18Comments
Resources
Tags
- Windows PowerShell1,156 Topics
- powershell335 Topics
- office 365272 Topics
- azure active directory139 Topics
- Windows Server127 Topics
- sharepoint127 Topics
- windows98 Topics
- azure95 Topics
- exchange91 Topics
- community54 Topics