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:
- 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 failGet-MsolUser
, which only accepts UPNs and ObjectIds. We need to accept that failure, tryGet-Mailbox
, and then on success use that Mailbox to runGet-MsolUser
again. - 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:
- We’re going to be using the variable
$Identity
as our input. - We need to handle $Identity being either a UPN or an ObjectId for
Get-MsolUser
. - We want to be storing these in variables for later use.
- 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…
I don’t understand the purpose of converting Identity to a GUID, mind explaining a bit more?
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.
I figgered it out. :p