Using variables with Invoke-Command in Remote PowerShell sessions in Exchange Online

As many of you probably know, Remote PowerShell sessions connected to Exchange Online run in the “no language” mode. This can easily be verified via:

(Get-PSSession | ? {($_.configurationname -eq "microsoft.exchange") -and ($_.Runspace.ConnectionInfo.ConnectionUri.Host -eq "outlook.office365.com")}).Runspace.SessionStateProxy.LanguageMode

Same applies to Skype for Business Online sessions, and in general is a pain in the royal behinds, as it prevents you from using variables, operators and everything else that can make your life easier. There are certain workarounds available, but in general they are still subject to limitations and can make the code very complex.

Another important limitation with Remote PowerShell sessions is the sessions stability. Everyone that has used some long running script in Exchange Online has run into this, and it’s not pretty. Some code optimizations are mandatory for such cases, and Microsoft has posted some guidance around them on the Exchange blog. Even those recommendations suffer from the aforementioned “no language mode” problem!

To better illustrate the issue, let’s take a look at the following example: we want to get a list of details for all the mailboxes in our company created after specific date. We can certainly use server-side filters to achieve this, which greatly increases efficiency:

Get-Mailbox -Filter {WhenMailboxCreated -gt "01 Jan 2016"}

With the recommendations from the blog article in mind however, we might want to use the Invoke-Command approach and return only the data that we need instead of the full objects. Using the $session variable to designate a currently opened ExO session, a working solution will look something like this:

$session = Get-PSSession -InstanceId (Get-OrganizationConfig).RunspaceId.Guid

Invoke-Command -Session $session -ScriptBlock { Get-Mailbox -Filter 'WhenMailboxCreated -gt "01 Jan 2016"' | Select-Object -Property Displayname, Identity, PrimarySMTPAddress, RecipientTypeDetails }

So far so good, but once we start looking into ways to make this a function, script or simply use some variable to designate the date, things start to get messy. The good news is it’s still doable though! Here’s an example that will work with PowerShell v3 and later, by means of exploiting the Using scope modifier:

$date = (Get-Date).AddDays(-250)

Invoke-Command -Session $session -ScriptBlock {Get-Mailbox -Filter ([scriptblock]::create("WhenMailboxCreated -gt '$using:date'")) | Select-Object -Property Displayname, Identity, PrimarySMTPAddress, RecipientTypeDetails }

The important bits: we need to use a Script block in order to prepare the filter syntax and we need to invoke the Using scope modifier to make sure the variable is evaluated locally. Remember, remote sessions running in “no language” mode do not allow you to set/reference remote variables!

For people still using older PowerShell versions (really?!), here’s a working example for V2:

Invoke-Command -Session $session -ScriptBlock {param($date) Get-Mailbox -Filter ([scriptblock]::create("WhenMailboxCreated -gt '$date'")) | Select-Object -Property Displayname, Identity, PrimarySMTPAddress, RecipientTypeDetails } -ArgumentList (Get-Date).AddDays(-250)

We can also use an example that does not require the need for using the Filter parameter and thus the scriptblock syntax inside the ScriptBlock parameter. For instance, if we want to get all mailboxes of the User and Shared type:

Invoke-Command -Session $session -ScriptBlock { Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox,SharedMailbox | Select-Object -Property Displayname,Identity,PrimarySMTPAddress,RecipientTypeDetails }

Works great, but if we want to make it dynamic, by passing the mailbox type as parameter or a variable, we need to use something like this instead:

$included = @("UserMailbox","SharedMailbox")

Invoke-Command -Session $session -ScriptBlock { Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails $Using:included | Select-Object -Property Displayname,Identity,PrimarySMTPAddress,RecipientTypeDetails }

Or in the old V2 format:

Invoke-Command -Session $session -ScriptBlock {param($log) Get-Mailbox -RecipientTypeDetails $log} -ArgumentList UserMailbox,TeamMailbox

So there you have it, a way to work around some of the restrictions of the “no language mode” for remote PowerShell sessions. It surely helped me simplify some of my scripts, by increasing their efficiency at the same time, so hopefully it will be useful to others as well!

4 thoughts on “Using variables with Invoke-Command in Remote PowerShell sessions in Exchange Online

  1. Andrew Gaskell says:

    Hi Vasil
    this is a very good article. I have throttling issues running Search-UnifiedAuditLog which returns >3K records. Your examples here work OK for me, but I cannot get the same to work with my use case using search-unifiedauditlog:

    $date = (Get-Date).AddDays(-1)
    $year = $date.Year
    $month = $date.Month
    $startOfMonth = Get-Date -Year $year -Month $month -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0
    $endOfMonth = ($startOfMonth).AddMonths(1).AddTicks(-1)

    Connect-ExchangeOnline
    $session = Get-PSSession -InstanceId (Get-OrganizationConfig).RunspaceId.Guid
    Invoke-Command -Session $session -ScriptBlock {Search-UnifiedAuditLog -StartDate ([scriptblock]::create(“‘$using:startofMonth'”)) -EndDate ([scriptblock]::create(“‘$using:endofMonth'”)) -SessionCommand ReturnLargeSet -resultsize 500 -Operations FileDownloaded -SiteID ‘f3325ce1-bbb3-4268-a983-086c5bb1c7b4’ | Select-Object -Property AuditData, UserIds, CreationDate}

    Gives me:

    Cannot process argument transformation on parameter ‘StartDate’. Cannot convert value “’05/01/2022 00:00:00′” to type
    “Microsoft.Exchange.ExchangeSystem.ExDateTime”. Error: “String was not recognized as a valid DateTime.”
    + CategoryInfo : InvalidData: (:) [Search-UnifiedAuditLog], ParameterBindin…mationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Search-UnifiedAuditLog
    + PSComputerName : outlook.office365.com

    I have tried *lots* of things with the dateformat to get this to work but not having any success. Would you have any ideas? Thanks.

    Reply
    1. Vasil Michev says:

      You’ve over complicating it:

      Invoke-Command -Session (Get-PSSession) -ScriptBlock {Search-UnifiedAuditLog -StartDate $using:startofMonth -EndDate $using:endofMonth -SessionCommand ReturnLargeSet -resultsize 500 -Operations FileDownloaded -SiteID ‘f3325ce1-bbb3-4268-a983-086c5bb1c7b4’ | Select-Object -Property AuditData, UserIds, CreationDate}
      Reply
  2. Tonino Bruno says:

    Hi Vasil,

    thanks for your awesome blog, it’s been very helpfull!

    I am trying to do a filter on EXO to find all the objects a user is manager of and ran into an issue where the DN of mailbox I am doing the query for contains an apostrophe. I can’t really find a way to get this correctly parsed into invoke-command using the methods you described above.

    Any thoughts?

    Sincerely,
    Tonino Bruno

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.