All-in-one Get-User cmdlet: Part 2 – Error Handling Get-MsolUser

So a big part of our all-in-one Get-User cmdlet Get-MITUser, is that it needs to try obtaining both the MsolUser from Office 365, and the Mailbox from Exchange Online.

Imagine this first as a manual process with “john@example.com”. It would look something like this:

PS> Get-MsolUser -UserPrincipalName john@example.com

UserPrincipalName  DisplayName isLicensed
-----------------  ----------- ----------
john@example.com   John Smith  True 
    
PS> Get-Mailbox -Identity John@example.com

Name            Alias      ServerName       ProhibitSendQuota
----            -----      ----------       -----------------
John            John       am2pr01mb0994    49.5 GB (53,150,220,288 bytes)

But in our Get-MITUser function, we are going to take a single -Identity parameter and use it with both Get-MSolUser and Get-Mailbox. We need to be prepared that one of these commands may fail:

  1. It may fail Get-MsolUser.
    For example, the Identity provided may be a Name, Alias, or Email Address (that doesn’t happen to be the UPN), all of which will fail Get-MsolUser, which only accepts UPNs and ObjectIds. We need to accept that failure, try Get-Mailbox, and then on success use that Mailbox to run Get-MsolUser again.
  2. It may fail Get-Mailbox.
    For example, the most likely reason for this is that this is a user without a Mailbox. This isn’t a failure as far as we are concerned; we want to indicate in our output that an MsolUser was found but not a Mailbox.

But when it does fail, we don’t necessarily want to start displaying errors to the user, at least not immediately. Again imagine this as a manual process, but this time with “John”. It would look something like this:

PS> Get-MsolUser -UserPrincipalName John
Get-MsolUser : User Not Found.  User: John.
At line:1 char:1
+ Get-MsolUser -UserPrincipalName John
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [Get-MsolUser], MicrosoftOnlineException
    + FullyQualifiedErrorId : Microsoft.Online.Administration.Automation.UserNotFoundException,Microsoft.Online.Administration.Automation.GetUser

PS> # The MsolUser with UPN "John" does not exist.
PS> # Let's see if the Mailbox "John" exists.
PS> Get-Mailbox -Identity John

Name            Alias      ServerName       ProhibitSendQuota
----            -----      ----------       -----------------
John            John       am2pr01mb0994    49.5 GB (53,150,220,288 bytes)

PS> # OK, the Mailbox exists.
PS> # Let's get the ExternalDirectoryObjectId of that mailbox.
PS> Get-Mailbox -Identity John | Select-Object -ExpandProperty ExternalDirectoryObjectId
17ff08b1-13bf-4f9d-85da-eece3971216e

PS> # Now let's get the MsolUser with that Guid as the ObjectId
PS> Get-MsolUser -ObjectId 17ff08b1-13bf-4f9d-85da-eece3971216e

UserPrincipalName  DisplayName isLicensed
-----------------  ----------- ----------
john@example.com   John Smith  True 

PS> # Great, we've found both the Mailbox and the MsolUser.
PS> # We can disregard that error above as we have achieved our goal.

What we need to do is put the above into our Get-MITUser cmdlet, rather than a manual process. Let’s start with the Get-MsolUser part, testing with a UPN that does not exist.

PS> Get-MsolUser -UserPrincipalName DoesNotExist@example.com
Get-MsolUser : User Not Found.  User: DoesNotExist@example.com.
At line:1 char:1
+ Get-MsolUser -UserPrincipalName DoesNotExist@example.com
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [Get-MsolUser], MicrosoftOnlineException
    + FullyQualifiedErrorId : Microsoft.Online.Administration.Automation.UserNotFoundException,Microsoft.Online.Administration.Automation.GetUser

Now the way to catch exceptions in PowerShell is to wrap the command in a Try / Catch block. So we try this with our command:

PS> try { 
    Get-MsolUser -UserPrincipalName DoesNotExist@example.com
} catch {
    Write-Host "I caught an error"
}
Get-MsolUser : User Not Found.  User: DoesNotExist@example.com.
At line:2 char:5
+     Get-MsolUser -UserPrincipalName DoesNotExist@example.com
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [Get-MsolUser], MicrosoftOnlineException
    + FullyQualifiedErrorId : Microsoft.Online.Administration.Automation.UserNotFoundException,Microsoft.Online.Administration.Automation.GetUser

In this case, no change. In this and most cases, the reason for this is that Get-MsolUser has issued a Non-terminating Error, rather than an Exception. Try / Catch only catches Exceptions. Thankfully there is a PowerShell standard way to turn such errors into exceptions – use the -ErrorAction parameter.

PS> try { 
    Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAction Stop
} catch {
    Write-Host "I caught an error"
}
I caught an error

The default ErrorAction is “Continue”, which displays non-terminating errors and then continues. Changing it to “Stop” issues an exception and then halts the command. But in a Try / Catch, we can catch that exception and handle it how we like.

Now we don’t want to silence all errors, we just want to silence user not found errors. You can tell catch to only catch certain types of errors. We can see from above that this is a Microsoft.Online.Administration.Automation.UserNotFoundException exception, so let’s try that:

PS> try { 
    Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAction Stop
} catch [Microsoft.Online.Administration.Automation.UserNotFoundException] {
    Write-Host "I caught an error"
}
Get-MsolUser : User Not Found.  User: DoesNotExist@example.com.
At line:2 char:5
+     Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAc ...
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [Get-MsolUser], MicrosoftOnlineException
    + FullyQualifiedErrorId : Microsoft.Online.Administration.Automation.UserNotFoundException,Microsoft.Online.Administration.Automation.GetUser

It didn’t work, so Microsoft.Online.Administration.Automation.UserNotFoundException must not be the type of exception. We can find out the exception type in a few ways. One way is that immediately after the error has been output, we can access it in the global $Error variable, which stores all errors in the session, with the newest in the 0th slot.

PS> $Error[0].ErrorRecord
Get-MsolUser : User Not Found.  User: DoesNotExist@example.com.
At line:2 char:5
+     Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAc ...
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [Get-MsolUser], MicrosoftOnlineException
    + FullyQualifiedErrorId : Microsoft.Online.Administration.Automation.UserNotFoundException,Microsoft.Online.Administration.Automation.GetUser

A property of an ErrorRecord is the Exception. We can pass this property to Get-Member, which will tell us the type:

PS> $Error[0].ErrorRecord.Exception | Get-Member


   TypeName: Microsoft.Online.Administration.Automation.MicrosoftOnlineException

Name             MemberType Definition                                                                                                                                   
----             ---------- ----------                                                                                                                                   
Equals           Method     bool Equals(System.Object obj), bool _Exception.Equals(System.Object obj)                                                                    
GetBaseException Method     System.Exception GetBaseException(), System.Exception _Exception.GetBaseException()                                                          
GetHashCode      Method     int GetHashCode(), int _Exception.GetHashCode()                                                                                              
GetObjectData    Method     void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context), void ISeri...
GetType          Method     type GetType(), type _Exception.GetType()                                                                                                    
ToString         Method     string ToString(), string _Exception.ToString()                                                                                              
Data             Property   System.Collections.IDictionary Data {get;}                                                                                                   
HelpLink         Property   string HelpLink {get;set;}                                                                                                                   
HResult          Property   int HResult {get;set;}                                                                                                                       
InnerException   Property   System.Exception InnerException {get;}                                                                                                       
Message          Property   string Message {get;}                                                                                                                        
Source           Property   string Source {get;set;}                                                                                                                     
StackTrace       Property   string StackTrace {get;}                                                                                                                     
TargetSite       Property   System.Reflection.MethodBase TargetSite {get;}      

We can now see that the exception type is the slightly different Microsoft.Online.Administration.Automation.MicrosoftOnlineException. We can see that this allows us to catch the error.

PS> try { 
    Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAction Stop
} catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] {
    Write-Host "I caught an error"
}
I caught an error

Now MicrosoftOnlineException is still a bit too a generic a category. From some brief testing we can see that other errors have the same exception type. For example if we never ran Connect-MsolService in the first place, when we run Get-MsolUser we get a different error “You must call the Connect-MsolService cmdlet before calling any other cmdlets.” instead of “User Not Found. User: DoesNotExist@example.com.”, however the exception type is the same for both.

Looking back at that Get-Member on the exception, we can see the “Message” property. What does that contain?

PS> $Error[0].ErrorRecord.Exception.Message
User Not Found.  User: DoesNotExist@example.com.

Perfect, this we can work with. It’s only “User Not Found” messages that we know we can handle, anything else we want to throw back to the user. So our Try / Catch block may look like this:

try { 
    Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAction Stop
} catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] {
    if ($_.Exception.Message -notmatch "^User Not Found.") {
        throw
    }
    Write-Host "I caught an error"
}

Within a catch block, $_ (“dollar underscore”) represents the error / exception that was caught. We can check the message to see if it’s a “User not found” error, either using -match/-notmatch with a regular expression, or with -like/-notlike with a simple string with wildcards. If it’s not a “User not found” error, then throw will rethrow the original error.

So for example, running this after correctly running Connect-MsolService shows the following:

PS> try { 
    Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAction Stop
} catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] {
    if ($_.Exception.Message -notmatch "^User Not Found.") {
        throw
    }
    Write-Host "I caught an error"
}
I caught an error

But running it before Connect-MsolService displays the error:

PS> try { 
    Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAction Stop
} catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] {
    if ($_.Exception.Message -notmatch "^User Not Found.") {
        throw
    }
    Write-Host "I caught an error"
}
Get-MsolUser : You must call the Connect-MsolService cmdlet before calling any other cmdlets.
At line:2 char:5
+     Get-MsolUser -UserPrincipalName DoesNotExist@example.com -ErrorAc ...
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [Get-MsolUser], MicrosoftOnlineException
    + FullyQualifiedErrorId : Microsoft.Online.Administration.Automation.MicrosoftOnlineException,Microsoft.Online.Administration.Automation.GetUser

Now of course, this is just part of our larger Get-MITUser function, so there are a few more changes we need to make. In particular:

  1. We’re going to be using the variable $Identity as our input.
  2. We need to handle $Identity being either a UPN or an ObjectId for Get-MsolUser.
  3. We want to be storing these in variables for later use.
  4. We want some more error checking.

The final snippet looks like this:

try {

    # Try converting $Identity to a Guid.
    # Note that Get-MsolUser attempts this exact process automatically
    # when called with the -ObjectId parameter.
    $Guid = $Identity -as [System.Guid]

    # If it can convert to a Guid, there's no way it's a UserPrincipalName,
    # if nothing else for the lack of @ sign.  So we only need to try one command.
    if ($Guid) {
        $MsolUser = Get-MsolUser -ObjectId $Guid -ErrorAction Stop
    } else {
        $MsolUser = Get-MsolUser -UserPrincipalName $Identity -ErrorAction Stop
    } #end else
    
    # Double check that we have actually obtained an MsolUser.
    if ($MsolUser) {
        $MsolUserFound = $true
    } else {
        $MsolUserFound = $false
    } #end else

} catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] {

    # We may need this message for later.    
    $MsolUserError = $_.Exception.Message

    if ($MsolUserError -match "^User Not Found.") {
        # We can handle a user not found, so no need to throw.
        $MsolUserFound = $false
    } else {
        # We don't (yet) know how to handle this error, so rethrow for the user to see.
        throw
    } #end else

} #end catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException]


So the end result of this, is that we have $MsolUserFound set to either $true or $false, and then either an $MsolUser or an $MsolUserError.

Next time we will be doing the same thing for Get-Mailbox. The above is pretty much all standard PowerShell techniques. You would think we could apply them the same to Get-Mailbox, right? Unfortunately this is not the case…

3 Comments

  1. Dean McKenzie

    I don’t understand the purpose of converting Identity to a GUID, mind explaining a bit more?

    Reply
    1. Ryan Milne (Post author)

      I’m glad you figured it out, but obviously I should explain it a bit better. In full detail:

      As you may know, a lot of Microsoft cmdlets like Get-ADUser (AD), Get-Mailbox (Exchange), or Get-CsUser (Skype for Business) take a single -Identity parameter. You give it your parameter, and it will look look it up in all supported attributes for a match.

      For example, according to TechNet, with Get-Mailbox, if you give it either the user principal name (UPN) OR the GUID, it can find the mailbox. (Or many other attributes too – Name, DisplayName, Alias…)

      Get-MsolUser isn’t like this. You can *either* call it with -UserPrincipalName and match the UPN exactly, *or* you can call it with -ObjectID and match the ObjectID (which is a GUID) exactly. We don’t want to worry about that, we want our functions and cmdlets to work like Get-ADUser.

      Now we could simply try Get-MsolUser up to two times – first with the Identity as the UPN and then if that fails again as an ObjectID, but that’s potentially a waste of a call to Get-MsolUser.

      Thankfully, something that is a GUID can’t also be a UPN – in particular a GUID cannot contain the @ symbol but a UPN must. So we can look at a string and know without doubt whether to try it against -ObjectId or against -UserPrincipalName.

      Get-MsolUser’s -ObjectId parameter is set to the type [System.Guid]. This means that when you feed it a string object, PowerShell first tries to convert it to a System.Guid before passing it to Get-MsolUser, much like if you had called [System.Guid]”myguid”. [System.Guid] is fairly flexible when casting a string – for example it works both with or without dashes (17ff08b1-13bf-4f9d-85da-eece3971216e vs 17ff08b113bf4f9d85daeece3971216e). We want our function to have the same flexibility.

      In order to do this, all we have to do is use PowerShell’s -as operator to *try* casting the string as a GUID, which uses the exact same type conversion as above. If it succeeds ($Guid is not empty), it’s a GUID. If it’s not ($Guid variable is empty) we know it’s not and can try it as a UPN.

      I hope that’s a bit clearer.

      Reply
  2. Dean

    I figgered it out. :p

    Reply

Leave a Comment

Your e-mail address will not be published. Required fields are marked *