internal/Get-MsGraphResults.ps1

<#
.SYNOPSIS
    Query Microsoft Graph API
.PARAMETER EnableInFilter
    Enables in filter by in on ids for requried uniqueIds; $filter={previous filter} and id in ({csv with ids})
    Should be more flexible than GetByIds, scalability to be tested to eventually replace getbyids
    This filter is currently experimental and will be subject to future change (move to DisableInFilterBatching).
.EXAMPLE
    PS C:\>Get-MsGraphResults 'users'
    Return query results for first page of users.
.EXAMPLE
    PS C:\>Get-MsGraphResults 'users' -ApiVersion beta
    Return query results for all users using the beta API.
.EXAMPLE
    PS C:\>Get-MsGraphResults 'users' -UniqueId 'user1@domain.com','user2@domain.com' -Select id,userPrincipalName,displayName
    Return id, userPrincipalName, and displayName for user1@domain.com and user2@domain.com.
#>

function Get-MsGraphResults {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        # Graph endpoint such as "users".
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [uri[]] $RelativeUri,
        # Specifies unique Id(s) for the URI endpoint. For example, users endpoint accepts Id or UPN.
        [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        #[ValidateNotNullOrEmpty()]
        [string[]] $UniqueId,
        # Filters properties (columns).
        [Parameter(Mandatory = $false)]
        [string[]] $Select,
        # Filters results (rows). https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
        [Parameter(Mandatory = $false)]
        [string] $Filter,
        # Specifies the page size of the result set.
        [Parameter(Mandatory = $false)]
        [int] $Top,
        # Include a count of the total number of items in a collection
        [Parameter(Mandatory = $false)]
        [switch] $Count,
        # Parameters such as "$orderby".
        [Parameter(Mandatory = $false)]
        [hashtable] $QueryParameters,
        # API Version.
        [Parameter(Mandatory = $false)]
        [ValidateSet('v1.0', 'beta')]
        [string] $ApiVersion = 'v1.0',
        # Specifies consistency level.
        [Parameter(Mandatory = $false)]
        [string] $ConsistencyLevel = "eventual",
        # Correlation Id (client-request-id).
        [Parameter(Mandatory = $false)]
        [guid] $CorrelationId = (New-Guid),
        # Total requests to calcuate progress bar when using pipeline.
        [Parameter(Mandatory = $false)]
        [int] $TotalRequests,
        # Copy OData Context to each result value.
        [Parameter(Mandatory = $false)]
        [switch] $KeepODataContext,
        # Add OData Type to each result value.
        [Parameter(Mandatory = $false)]
        [switch] $AddODataType,
        # Incapsulate member and owner reference calls with a parent object.
        [Parameter(Mandatory = $false)]
        [switch] $IncapsulateReferenceListInParentObject,
        # Group results in array by request.
        [Parameter(Mandatory = $false)]
        [switch] $GroupOutputByRequest,
        # Disable deduplication of UniqueId values.
        [Parameter(Mandatory = $false)]
        [switch] $DisableUniqueIdDeduplication,
        # Only return first page of results.
        [Parameter(Mandatory = $false)]
        [switch] $DisablePaging,
        # Disable consolidating uniqueIds using getByIds endpoint
        [Parameter(Mandatory = $false)]
        [switch] $DisableGetByIdsBatching,
        # Specify GetByIds Batch size.
        [Parameter(Mandatory = $false)]
        [int] $GetByIdsBatchSize = 1000,
        # Enables in filter by in on ids for requried uniqueIds; $filter={previous filter} and id in ({csv with ids})
        # Should be more flexible than GetByIds, scalability to be tested to eventually replace getbyids
        [Parameter(Mandatory = $false)]
        [switch] $EnableInFilter,
        [Parameter(Mandatory = $false)]
        [int] $InFilterBatchSize = 15,
        # Force individual requests to MS Graph.
        [Parameter(Mandatory = $false)]
        [switch] $DisableBatching,
        # Specify Batch size.
        [Parameter(Mandatory = $false)]
        [int] $BatchSize = 20,
        # Base URL for Microsoft Graph API.
        [Parameter(Mandatory = $false)]
        [uri] $GraphBaseUri = $script:mapMgEnvironmentToMgEndpoint[$script:ConnectState.CloudEnvironment]
    )

    begin {

        if ($EnableInFilter) {
            Write-Verbose "EnableInFilter switch used: this filter is currently experimental and will be subject to future change (move to DisableInFilterBatching)"
        }

        [uri] $uriGraphVersionBase = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $ApiVersion)
        $listRequests = New-Object 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[pscustomobject]]'
        $listRequests.Add($uriGraphVersionBase.AbsoluteUri, (New-Object 'System.Collections.Generic.List[pscustomobject]'))
        [System.Collections.Generic.List[guid]] $listIds = New-Object 'System.Collections.Generic.List[guid]'
        [System.Collections.Generic.HashSet[uri]] $hashUri = New-Object 'System.Collections.Generic.HashSet[uri]'
        $ProgressState = Start-Progress -Activity 'Microsoft Graph Requests' -Total $TotalRequests

        function Catch-MsGraphError {
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [System.Management.Automation.ErrorRecord] $ErrorRecord
            )

            # throw error record directly if no response is found on the exception
            if (!$_.Exception.psobject.Properties.Name.Contains('Response')) {
                throw $ErrorRecord
            }
            
            $ResponseDetail = Get-MsGraphResponseDetail $_
            Write-Debug -Message (ConvertTo-Json ([PSCustomObject]$ResponseDetail) -Depth 3)

            if ($ResponseDetail['ContentParsed']) {
                ## Terminating errors with specific codes
                if ($ResponseDetail['ContentParsed'].error.code -in ('Authentication_ExpiredToken', 'Service_ServiceUnavailable', 'Request_UnsupportedQuery')) {
                    #Write-AppInsightsException -ErrorRecord $_ -OrderedProperties $ResponseDetail # Not needed when calling function has try finally to write terminating errors
                    Write-Error -Exception $_.Exception -Message $ResponseDetail['ContentParsed'].error.message -ErrorId $ResponseDetail['ContentParsed'].error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorAction Stop
                }
                else {
                    ## Ignore errors with specific codes else display non-terminating error
                    if ($ResponseDetail['ContentParsed'].error.code -eq 'Request_ResourceNotFound') {
                        Write-Error -Exception $_.Exception -Message $ResponseDetail['ContentParsed'].error.message -ErrorId $ResponseDetail['ContentParsed'].error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorVariable cmdError -ErrorAction SilentlyContinue
                        #Write-Warning $ResponseDetail['ContentParsed'].error.message
                    }
                    else {
                        Write-Error -Exception $_.Exception -Message $ResponseDetail['ContentParsed'].error.message -ErrorId $ResponseDetail['ContentParsed'].error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorVariable cmdError
                    }
                    
                    if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') }
                    Write-AppInsightsException -ErrorRecord $cmdError -OrderedProperties $ResponseDetail
                }
            }
            else { throw $ErrorRecord }
        }

        function Test-MsGraphBatchError ($BatchResponse, $BatchRequest) {
            if ($BatchResponse.status -ne '200') {
                Write-Debug -Message (ConvertTo-Json $BatchResponse -Depth 3)

                ## Terminating errors with specific codes
                if ($BatchResponse.body.error.code -in ('Authentication_ExpiredToken','Service_ServiceUnavailable','Request_UnsupportedQuery')) {
                    Write-Error -Message $BatchResponse.body.error.message -ErrorId $BatchResponse.body.error.code -ErrorAction Stop
                }
                else {
                    ## Ignore errors with specific codes else display non-terminating error
                    if ($BatchResponse.body.error.code -eq 'Request_ResourceNotFound') {
                        Write-Error -Message $BatchResponse.body.error.message -ErrorId $BatchResponse.body.error.code -ErrorVariable cmdError -ErrorAction SilentlyContinue
                        #Write-Warning $BatchResponse.body.error.message
                    }
                    else {
                        Write-Error -Message $BatchResponse.body.error.message -ErrorId $BatchResponse.body.error.code -ErrorVariable cmdError
                    }
                    # generate extra properties for the exception
                    $ResponseDetail = Get-MsGraphResponseDetail $BatchResponse
                    if ($BatchRequest) { $ResponseDetail["Request"] = "{0} {1}" -f $BatchRequest.method, $BatchRequest.url }

                    if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') }
                    Write-AppInsightsException -ErrorRecord $cmdError -OrderedProperties $ResponseDetail
                }
                return $true
            }
            return $false
        }

        function Add-MsGraphRequest {
            param (
                # A collection of request objects.
                [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
                [object[]] $Requests,
                # Base URL for Microsoft Graph API.
                [Parameter(Mandatory = $false)]
                [uri] $GraphBaseUri = 'https://graph.microsoft.com/'
            )

            process {
                foreach ($Request in $Requests) {
                    if ($DisableBatching) {
                        if ($ProgressState) { Update-Progress $ProgressState -CurrentOperation ('{0} {1}' -f $Request.method.ToUpper(), $Request.url) -IncrementBy 1 }
                        Invoke-MsGraphRequest $Request -GraphBaseUri $GraphBaseUri
                    }
                    else {
                        $listRequests[$GraphBaseUri].Add($Request)
                        ## Invoke when there are enough for a batch
                        while ($listRequests[$GraphBaseUri].Count -ge $BatchSize) {
                            Invoke-MsGraphBatchRequest $listRequests[$GraphBaseUri][0..($BatchSize - 1)] -BatchSize $BatchSize -ProgressState $ProgressState -GraphBaseUri $GraphBaseUri
                            $listRequests[$GraphBaseUri].RemoveRange(0, $BatchSize)
                        }
                    }
                }
            }
        }

        function Invoke-MsGraphBatchRequest {
            param (
                # A collection of request objects.
                [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
                [object[]] $Requests,
                # Specify Batch size.
                [Parameter(Mandatory = $false)]
                [int] $BatchSize = 20,
                # Use external progress object.
                [Parameter(Mandatory = $false)]
                [psobject] $ProgressState,
                # Base URL for Microsoft Graph API.
                [Parameter(Mandatory = $false)]
                [uri] $GraphBaseUri = 'https://graph.microsoft.com/'
            )

            begin {
                [bool] $ExternalProgress = $false
                if ($ProgressState) { $ExternalProgress = $true }
                else {
                    $ProgressState = Start-Progress -Activity 'Microsoft Graph Requests - Batched' -Total $Requests.Count
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                }
                [uri] $uriEndpoint = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, '$batch')
                $listRequests = New-Object 'System.Collections.Generic.List[pscustomobject]'
            }

            process {
                foreach ($Request in $Requests) {
                    $listRequests.Add($Request)
                }
            }

            end {
                [array] $BatchRequests = New-MsGraphBatchRequest $listRequests -BatchSize $BatchSize
                for ($iRequest = 0; $iRequest -lt $BatchRequests.Count; $iRequest++) {
                    if ($ProgressState.Total -gt $BatchSize) {
                        Update-Progress $ProgressState -CurrentOperation ('{0} {1}' -f $BatchRequests[$iRequest].method.ToUpper(), $BatchRequests[$iRequest].url) -IncrementBy $BatchRequests[$iRequest].body.requests.Count
                    }
                    $resultsBatch = Invoke-MsGraphRequest $BatchRequests[$iRequest] -NoAppInsights -GraphBaseUri $GraphBaseUri

                    [array] $resultsBatch = $resultsBatch.responses | Sort-Object -Property { [int]$_.id }

                    foreach ($results in ($resultsBatch)) {
                        # check if batch result failed and call the endpoint or throw
                        if ($results.status -eq "429") {
                            [int] $RetryAfter = Get-ObjectPropertyValue $results headers 'Retry-After'
                            [double] $SecondsRemaining = $RetryAfter
                            $Date = Get-ObjectPropertyValue $results body error innerError 'date'
                            if ($Date) {
                                if ($PSVersionTable.PSVersion -ge [version]'7.1') { $CurrentTime = Get-Date -AsUTC }
                                else { $CurrentTime = [datetime]::UtcNow }
                                $SecondsRemaining = $(([datetime]$Date).AddSeconds($RetryAfter) - $CurrentTime).TotalSeconds
                            }

                            if ($SecondsRemaining -gt 0) {
                                Write-Warning ("Request from batch was throttled and will attempt retry after {0:0}s." -f $SecondsRemaining)
                                Start-Sleep -Seconds $SecondsRemaining
                            }
                            else {
                                Write-Warning "Request from batch was throttled and will attempt retry."
                            }

                            Invoke-MsGraphRequest $request -NoAppInsights -GraphBaseUri $GraphBaseUri
                            continue
                        }
                        $currentRequest = $BatchRequests[$iRequest] | Where-Object {$_.id -eq $results.id}
                        if (!(Test-MsGraphBatchError $results $currentRequest)) {
                            if ($IncapsulateReferenceListInParentObject -and $listRequests[$results.id].url -match '.*/(.+)/(.+)/((?:transitive)?members|owners)') {
                                [PSCustomObject]@{
                                    id            = $Matches[2]
                                    '@odata.type' = '#{0}' -f (Get-MsGraphEntityType $GraphBaseUri.AbsoluteUri -EntityName $Matches[1])
                                    $Matches[3]   = Complete-MsGraphResult $results.body -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest -Request $listRequests[$results.id] -GraphBaseUri $GraphBaseUri
                                }
                            }
                            else {
                                Complete-MsGraphResult $results.body -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest:$GroupOutputByRequest -Request $listRequests[$results.id] -GraphBaseUri $GraphBaseUri
                            }
                        }
                    }
                }

                if (!$ExternalProgress) {
                    $Stopwatch.Stop()
                    Write-AppInsightsDependency ('{0} {1}' -f 'POST', $uriEndpoint.AbsolutePath) -Type 'MS Graph' -Data ("{0} {1}`r`n`r`n{2}" -f 'POST', $uriEndpoint.AbsoluteUri, ('{{"requests":[...{0}...]}}' -f $listRequests.Count)) -Duration $Stopwatch.Elapsed -Success ($null -ne $resultsBatch)
                    Stop-Progress $ProgressState
                }
            }
        }

        function Invoke-MsGraphRequest {
            param (
                # A collection of request objects.
                [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
                [psobject] $Request,
                # Do not write application insights dependency.
                [Parameter(Mandatory = $false)]
                [switch] $NoAppInsights,
                # Base URL for Microsoft Graph API.
                [Parameter(Mandatory = $false)]
                [uri] $GraphBaseUri = 'https://graph.microsoft.com/',
                # Number of retries in case of throttling
                [Parameter(Mandatory = $false)]
                [int] $MaxRetries = 5,
                # Default Retry-After value
                [Parameter(Mandatory = $false)]
                [int] $RetryAfter = 2
            )

            process {
                [uri] $uriEndpoint = $Request.url
                if (!$uriEndpoint.IsAbsoluteUri) {
                    $uriEndpoint = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $Request.url.TrimStart('/'))
                }
                #if ($uriEndpoint.Segments -contains 'directoryObjects/') { $NoAppInsights = $true }

                [hashtable] $paramInvokeRestMethod = @{
                    Method = $Request.method
                    Uri    = $uriEndpoint
                }
                if ($Request.psobject.Properties.Name -contains 'headers') { $paramInvokeRestMethod.Add('Headers', $Request.headers) }
                if ($Request.psobject.Properties.Name -contains 'body') {
                    $paramInvokeRestMethod.Add('Body', ($Request.body | ConvertTo-Json -Depth 10 -Compress))
                    $paramInvokeRestMethod.Add('ContentType', 'application/json')
                }

                ## Get results
                $results = $null
                $MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop
                if (!$NoAppInsights) { $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() }
                try {
                    for($Retries = 0; $Retries -le $MaxRetries; $Retries++) {
                        try {
                            $results = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing @paramInvokeRestMethod -ErrorAction Stop
                            break  # break the loop if no error was raised
                        }
                        catch {
                            ## Retry request if response returns error or indicates throttling
                            # Windows PowerShell WebException Example: $_.Exception.Status -eq 'Timeout' # Response is also null because there was no response.
                            # Example: $_.Exception.Response.StatusCode.value__ -eq 429 # Throttling
                            # Example: $_.Exception.Response.StatusCode.value__ -eq 503 # ServiceUnavailable
                            #if ($Retries -lt $MaxRetries -and ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429 -or (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 503 -or !(Get-ObjectPropertyValue $_ Exception Response))) {
                            if ($Retries -lt $MaxRetries -and (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -notin 400,403) {
                                $ResponseDetail = Get-MsGraphResponseDetail $_
                                if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') }
                                Write-AppInsightsException -ErrorRecord $_ -OrderedProperties $ResponseDetail

                                # Get the retry after header
                                try {
                                    $RetryAfter = $_.Exception.Response.Headers.GetValues('Retry-After')[0]
                                }
                                catch {
                                    if ($Retries -gt 0) { $RetryAfter *= 2 }
                                }

                                if ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429) {
                                    Write-Warning "Request was throttled and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s."
                                }
                                else {
                                    Write-Warning "Request returned error and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s."
                                }
                                Start-Sleep -Seconds $RetryAfter
                            }
                            else {
                                # catch error if it was the last try
                                Catch-MsGraphError $_
                                break  # break the loop if error was not due to throttling
                            }
                        }
                    }
                    if ($results) {
                        if ($IncapsulateReferenceListInParentObject -and $Request.url -match '.*/(.+)/(.+)/((?:transitive)?members|owners)') {
                            [PSCustomObject]@{
                                id            = $Matches[2]
                                '@odata.type' = '#{0}' -f (Get-MsGraphEntityType $GraphBaseUri.AbsoluteUri -EntityName $Matches[1])
                                $Matches[3]   = Complete-MsGraphResult $results -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest -Request $Request -GraphBaseUri $GraphBaseUri -MaxRetries $MaxRetries -RetryAfter $RetryAfter
                            }
                        }
                        else {
                            Complete-MsGraphResult $results -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest:$GroupOutputByRequest -Request $Request -GraphBaseUri $GraphBaseUri -MaxRetries $MaxRetries -RetryAfter $RetryAfter
                        }       
                    }
                }
                finally {
                    if (!$NoAppInsights) {
                        $Stopwatch.Stop()
                        Write-AppInsightsDependency ('{0} {1}' -f $Request.method.ToUpper(), $uriEndpoint.AbsolutePath) -Type 'MS Graph' -Data ('{0} {1}' -f $Request.method.ToUpper(), $uriEndpoint.AbsoluteUri) -Duration $Stopwatch.Elapsed -Success ($null -ne $results)
                    }
                }
            }
        }

        function Complete-MsGraphResult {
            param (
                # Results from MS Graph API.
                [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
                [object[]] $Results,
                # Only return first page of results.
                [Parameter(Mandatory = $false)]
                [switch] $DisablePaging,
                # Copy ODataContext to each result value.
                [Parameter(Mandatory = $false)]
                [switch] $KeepODataContext,
                # Add ODataType to each result value.
                [Parameter(Mandatory = $false)]
                [switch] $AddODataType,
                # Group results in array by request.
                [Parameter(Mandatory = $false)]
                [switch] $GroupOutputByRequest,
                # MS Graph request object.
                [Parameter(Mandatory = $false)]
                [psobject] $Request,
                # Base URL for Microsoft Graph API.
                [Parameter(Mandatory = $false)]
                [uri] $GraphBaseUri = 'https://graph.microsoft.com/',
                # Number of retries in case of throttling
                [Parameter(Mandatory = $false)]
                [int] $MaxRetries = 5,
                # Default Retry-After value
                [Parameter(Mandatory = $false)]
                [int] $RetryAfter = 2
            )

            begin {
                [System.Collections.Generic.List[object]] $listOutput = New-Object 'System.Collections.Generic.List[object]'
            }

            process {
                foreach ($Result in $Results) {
                    $Output = Expand-MsGraphResult $Result -RawOutput:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType
                    if ($GroupOutputByRequest -and $Output) { $listOutput.AddRange([array]$Output) }
                    else { $Output }

                    if (!$DisablePaging -and $Result) {
                        if (Get-ObjectPropertyValue $Result '@odata.nextLink') {
                            [uri] $uriEndpoint = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $Request.url.TrimStart('/'))
                            [int] $Total = Get-MsGraphResultsCount $uriEndpoint -GraphBaseUri $GraphBaseUri
                            $Activity = ('{0} {1}' -f $Request.method.ToUpper(), $uriEndpoint.AbsolutePath)
                            $ProgressState = Start-Progress -Activity $Activity -Total $Total
                            $ProgressState.CurrentIteration = $Result.value.Count
                            try {
                                while (Get-ObjectPropertyValue $Result '@odata.nextLink') {
                                    Update-Progress $ProgressState -IncrementBy $Result.value.Count
                                    $nextLink = $Result.'@odata.nextLink'
                                    $MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop
                                    $Result = $null
                                    for ($Retries = 0; $Retries -le $MaxRetries; $Retries++) {
                                        try {
                                            $Result = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing -Method Get -Uri $nextLink -Headers $Request.headers -ErrorAction Stop
                                            break  # break the loop if no error was raised
                                        }
                                        catch {
                                            ## Retry request if response returns error or indicates throttling
                                            #if ($Retries -lt $MaxRetries -and ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429 -or (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 503 -or !(Get-ObjectPropertyValue $_ Exception Response))) {
                                            if ($Retries -lt $MaxRetries -and (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -notin 400, 403) {
                                                $ResponseDetail = Get-MsGraphResponseDetail $_
                                                if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') }
                                                Write-AppInsightsException -ErrorRecord $_ -OrderedProperties $ResponseDetail

                                                # Get the retry after header
                                                try {
                                                    $RetryAfter = $_.Exception.Response.Headers.GetValues('Retry-After')[0]
                                                }
                                                catch {
                                                    if ($Retries -gt 0) { $RetryAfter *= 2 }
                                                }

                                                if ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429) {
                                                    Write-Warning "Request was throttled and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s."
                                                }
                                                else {
                                                    Write-Warning "Request returned error and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s."
                                                }
                                                Start-Sleep -Seconds $RetryAfter
                                            }
                                            else {
                                                # catch error if it was the last try
                                                Catch-MsGraphError $_
                                                break  # break the loop if error was not due to throttling
                                            }
                                        }
                                    }
                                    if ($Result) {
                                        $Output = Expand-MsGraphResult $Result -RawOutput:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType
                                        if ($GroupOutputByRequest -and $Output) { $listOutput.AddRange([array]$Output) }
                                        else { $Output }
                                    }
                                }
                            }
                            finally {
                                Stop-Progress $ProgressState
                            }
                        }
                    }
                }
            }

            end {
                if ($GroupOutputByRequest) { Write-Output $listOutput.ToArray() -NoEnumerate }
            }
        }
    }

    process {
        ## Initialize
        if ($PSBoundParameters.ContainsKey('UniqueId') -and !$UniqueId) { return }
        if ($RelativeUri.OriginalString -eq $UniqueId) { $UniqueId = $null }  # Pipeline string/uri input binds to both parameters so default to just uri

        ## Process Each RelativeUri
        foreach ($uri in $RelativeUri) {
            [string] $BaseUri = $uriGraphVersionBase.AbsoluteUri
            if ($uri.IsAbsoluteUri) {
                if ($uri.AbsoluteUri -match '^https://(.+?)/(v1.0|beta)?') { $BaseUri = $Matches[0] }
                if (!$listRequests.ContainsKey($BaseUri)) { $listRequests.Add($BaseUri, (New-Object 'System.Collections.Generic.List[pscustomobject]')) }
                $uriQueryEndpoint = New-Object System.UriBuilder -ArgumentList $uri
            }
            else { $uriQueryEndpoint = New-Object System.UriBuilder -ArgumentList ([IO.Path]::Combine($BaseUri, $uri)) }

            ## Combine query parameters from URI and cmdlet parameters
            [hashtable] $QueryParametersFinal = @{ }
            if ($uriQueryEndpoint.Query) {
                $QueryParametersFinal = ConvertFrom-QueryString $uriQueryEndpoint.Query -AsHashtable
                if ($QueryParameters) {
                    foreach ($ParameterName in $QueryParameters.Keys) {
                        $QueryParametersFinal[$ParameterName] = $QueryParameters[$ParameterName]
                    }
                }
            }
            elseif ($QueryParameters) { $QueryParametersFinal = $QueryParameters }
            if ($Select) { $QueryParametersFinal['$select'] = $Select -join ',' }
            if ($Filter) { $QueryParametersFinal['$filter'] = $Filter }
            if ($Top) { $QueryParametersFinal['$top'] = $Top }
            if ($PSBoundParameters.ContainsKey('Count')) { $QueryParametersFinal['$count'] = ([string]$Count).ToLower() }
            $uriQueryEndpoint.Query = ConvertTo-QueryString $QueryParametersFinal

            ## Expand with UniqueIds
            if ($UniqueId) {
                foreach ($id in $UniqueId) {
                    if ($id) {
                        ## If the URI contains '{0}', then replace it with Unique Id.
                        if ($uriQueryEndpoint.Uri.AbsoluteUri.Contains('%7B0%7D')) {
                            $uriQueryEndpointUniqueId = New-Object System.UriBuilder -ArgumentList ([System.Net.WebUtility]::UrlDecode($uriQueryEndpoint.Uri.AbsoluteUri) -f $id)
                        }
                        else {
                            $uriQueryEndpointUniqueId = New-Object System.UriBuilder -ArgumentList $uriQueryEndpoint.Uri
                            $uriQueryEndpointUniqueId.Path = ([IO.Path]::Combine($uriQueryEndpointUniqueId.Path, $id))
                        }
                        if ($DisableUniqueIdDeduplication -or $hashUri.Add($uriQueryEndpointUniqueId.Uri)) {
                            if ($EnableInFilter -and $id -match '^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') {
                                $listIds.Add($id)
                                while($listIds.Count -ge $InFilterBatchSize) {
                                    # go back to initial uri (without appending id)
                                    $uriQueryEndpointUniqueId = New-Object System.UriBuilder -ArgumentList $uriQueryEndpoint.Uri
                                    # get the query parameters
                                    $QueryParametersInIds = ConvertFrom-QueryString $uriQueryEndpoint.Query -AsHashtable
                                    # get the ids to query
                                    $filterids = $listIds[0..($InFilterBatchSize - 1)]
                                    # append them to "$filter"
                                    if ($QueryParametersInIds.ContainsKey('$filter')) {
                                        $QueryParametersInIds['$filter'] = "($($QueryParametersInIds['$filter'])) and id in ('$($filterids -join "','")')"
                                    } else  {
                                        $QueryParametersInIds['$filter'] = "id in ('$($filterids -join "','")')"
                                    }
                                    # update query
                                    $uriQueryEndpointUniqueId.Query = ConvertTo-QueryString $QueryParametersInIds
                                    # add new batch request
                                    New-MsGraphRequest $uriQueryEndpointUniqueId.Uri -Headers @{ 'client-request-id' = $CorrelationId; ConsistencyLevel = $ConsistencyLevel } | Add-MsGraphRequest -GraphBaseUri $BaseUri
                                    # remove ids from ids to request
                                    $listIds.RemoveRange(0, $InFilterBatchSize)
                                    # update progress
                                    if ($ProgressState) { $ProgressState.CurrentIteration += $InFilterBatchSize - 1 }
                                }
                            }
                            elseif (!$DisableGetByIdsBatching -and $id -match '^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$' -and $uriQueryEndpoint.Uri.Segments.Count -eq 3 -and $uriQueryEndpoint.Uri.Segments[2] -in ('directoryObjects', 'users', 'groups', 'devices', 'servicePrincipals', 'applications') -and ($QueryParametersFinal.Count -eq 0 -or ($QueryParametersFinal.Count -eq 1 -and $QueryParametersFinal.ContainsKey('$select')))) {
                                $listIds.Add($id)
                                while ($listIds.Count -ge $GetByIdsBatchSize) {
                                    New-MsGraphGetByIdsRequest $listIds[0..($GetByIdsBatchSize - 1)] -Types $uriQueryEndpoint.Uri.Segments[2].TrimEnd('s') -Select $QueryParametersFinal['$select'] -BatchSize $GetByIdsBatchSize | Add-MsGraphRequest -GraphBaseUri $BaseUri
                                    $listIds.RemoveRange(0, $GetByIdsBatchSize)
                                    if ($ProgressState) { $ProgressState.CurrentIteration += $GetByIdsBatchSize - 1 }
                                }
                            }
                            else {
                                New-MsGraphRequest $uriQueryEndpointUniqueId.Uri -Headers @{ 'client-request-id' = $CorrelationId; ConsistencyLevel = $ConsistencyLevel } | Add-MsGraphRequest -GraphBaseUri $BaseUri
                            }
                        }
                        elseif ($ProgressState) { $ProgressState.Total -= 1 }
                    }
                    elseif ($ProgressState) { $ProgressState.Total -= 1 }
                }
            }
            else {
                New-MsGraphRequest $uriQueryEndpoint.Uri -Headers @{ 'client-request-id' = $CorrelationId; ConsistencyLevel = $ConsistencyLevel } | Add-MsGraphRequest -GraphBaseUri $BaseUri
            }
        }
    }

    end {
        ## Complete Remaining Ids
        if ($listIds.Count -gt 0) {
            New-MsGraphGetByIdsRequest $listIds -Types $uriQueryEndpoint.Uri.Segments[2].TrimEnd('s') -Select $QueryParametersFinal['$select'] -BatchSize $GetByIdsBatchSize | Add-MsGraphRequest -GraphBaseUri $BaseUri
            if ($ProgressState) { $ProgressState.CurrentIteration += $listIds.Count - 1 }
        }
        ## Finish requests
        foreach ($BaseUri in $listRequests.Keys) {
            if ($listRequests[$BaseUri].Count -eq 1) {
                Invoke-MSGraphRequest $listRequests[$BaseUri][0] -GraphBaseUri $BaseUri
            }
            elseif ($listRequests[$BaseUri].Count -gt 0) {
                Invoke-MsGraphBatchRequest $listRequests[$BaseUri] -BatchSize $BatchSize -ProgressState $ProgressState -GraphBaseUri $BaseUri
            }
            if (!$DisableBatching -and $ProgressState -and $ProgressState.CurrentIteration -gt 1) {
                [uri] $uriEndpoint = [IO.Path]::Combine($BaseUri, '$batch')
                Write-AppInsightsDependency ('{0} {1}' -f 'POST', $uriEndpoint.AbsolutePath) -Type 'MS Graph' -Data ("{0} {1}`r`n`r`n{2}" -f 'POST', $uriEndpoint.AbsoluteUri, ('{{"requests":[...{0}...]}}' -f $ProgressState.CurrentIteration)) -Duration $ProgressState.Stopwatch.Elapsed -Success $?
            }
        }
        ## Clean-up
        if ($ProgressState) { Stop-Progress $ProgressState }
    }
}



<#
.SYNOPSIS
    New request object containing Microsoft Graph API details.
.EXAMPLE
    PS C:\>New-MsGraphRequest 'users'
    Return request object for GET /users.
.EXAMPLE
    PS C:\>New-MsGraphRequest -Method Get -Uri 'https://graph.microsoft.com/v1.0/users'
    Return request object for GET /users.
.EXAMPLE
    PS C:\>New-MsGraphRequest -Method Patch -Uri 'users/{id}' -Body ([PsCustomObject]{ displayName = "Joe Cool" }
    Return request object for PATCH /users/{id} with a body payload to update the displayName.
#>

function New-MsGraphRequest {
    [CmdletBinding()]
    param (
        # Specifies the method used for the web request.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [int] $RequestId = 0,
        # Specifies the method used for the web request.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('Get', 'Head', 'Post', 'Put', 'Delete', 'Trace', 'Options', 'Merge', 'Patch')]
        [string] $Method = 'Get',
        # Specifies the Uniform Resource Identifier (URI) of the Internet resource to which the web request is sent.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [uri[]] $Uri,
        # Specifies the headers of the web request.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [hashtable] $Headers,
        # Specifies the body of the request.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [pscustomobject] $Body
    )

    process {
        if (!$Headers) { $Headers = @{} }
        for ($iRequest = 0; $iRequest -lt $Uri.Count; $iRequest++) {
            if ($Body) {
                if (!$Headers.ContainsKey('Content-Type')) { $Headers.Add('Content-Type', 'application/json') }
            }
            [string] $url = $Uri[$iRequest].PathAndQuery
            if (!$url) { $url = $Uri[$iRequest].ToString() }
            [pscustomobject]@{
                id      = $RequestId + $iRequest
                method  = $Method.ToUpper()
                url     = $url -replace '^(https://.+?/)?/?(v1.0/|beta/)?', '/'
                headers = $Headers
                body    = $Body
            }
        }
    }
}

function New-MsGraphGetByIdsRequest {
    [CmdletBinding()]
    param (
        # A collection of IDs for which to return objects.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [guid[]] $Ids,
        # A collection of resource types that specifies the set of resource collections to search.
        [Parameter(Mandatory = $false)]
        [string[]] $Types,
        # Filters properties (columns).
        [Parameter(Mandatory = $false)]
        [string[]] $Select,
        # Specify Batch size.
        [Parameter(Mandatory = $false)]
        [int] $BatchSize = 1000
    )

    begin {
        $Types = $Types | Where-Object { $_ -ne 'directoryObject' }
        if (!$Select) { $Select = "*" }
        $listIds = New-Object 'System.Collections.Generic.List[guid]'
    }

    process {
        foreach ($Id in $Ids) {
            $listIds.Add($Id)

            ## Process IDs when a full batch is reached
            while ($listIds.Count -ge $BatchSize) {
                New-MsGraphRequest ('/directoryObjects/getByIds?$select={0}' -f ($Select -join ',')) -Method Post -Headers @{ 'Content-Type' = 'application/json' } -Body ([PSCustomObject]@{
                        ids   = $listIds[0..($BatchSize - 1)]
                        types = $Types
                    })
                $listIds.RemoveRange(0, $BatchSize)
            }
        }
    }

    end {
        ## Process any remaining IDs
        if ($listIds.Count -gt 0) {
            New-MsGraphRequest ('/directoryObjects/getByIds?$select={0}' -f ($Select -join ',')) -Method Post -Headers @{ 'Content-Type' = 'application/json' } -Body ([PSCustomObject]@{
                    ids   = $listIds
                    types = $Types
                })
        }
    }
}

function New-MsGraphBatchRequest {
    [CmdletBinding()]
    param (
        # A collection of request objects.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object[]] $Requests,
        # Specify Batch size.
        [Parameter(Mandatory = $false)]
        [int] $BatchSize = 20,
        # Specify depth of nested batches. MS Graph does not currently support batch nesting.
        [Parameter(Mandatory = $false)]
        [int] $Depth = 1
    )

    process {
        for ($iRequest = 0; $iRequest -lt $Requests.Count; $iRequest += [System.Math]::Pow($BatchSize, $Depth)) {
            $indexEnd = [System.Math]::Min($iRequest + [System.Math]::Pow($BatchSize, $Depth) - 1, $Requests.Count - 1)

            ## Reset ID Order
            for ($iId = $iRequest; $iId -le $indexEnd; $iId++) {
                $Requests[$iId].id = $iId
            }

            ## Generate Batch Request
            if ($Depth -gt 1) {
                $BatchRequest = New-MsGraphBatchRequest $Requests[$iRequest..$indexEnd] -Depth ($Depth - 1)
            }
            else {
                $BatchRequest = $Requests[$iRequest..$indexEnd]
            }

            New-MsGraphRequest -RequestId $iRequest -Method Post -Uri '/$batch' -Headers @{ 'Content-Type' = 'application/json' } -Body ([PSCustomObject]@{
                    requests = $BatchRequest
                })
        }
    }
}

function Get-MsGraphMetadata {
    param (
        # Metadata URL for Microsoft Graph API.
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)]
        [uri] $Uri = 'https://graph.microsoft.com/v1.0/$metadata',
        # Force a refresh of metadata.
        [Parameter(Mandatory = $false)]
        [switch] $ForceRefresh
    )

    if (!(Get-Variable MsGraphMetadataCache -Scope Script -ErrorAction SilentlyContinue)) { New-Variable -Name MsGraphMetadataCache -Scope Script -Value (New-Object 'System.Collections.Generic.Dictionary[string,xml]') }
    if (!$Uri.AbsolutePath.EndsWith('$metadata')) { $Uri = ([IO.Path]::Combine($Uri.AbsoluteUri, '$metadata')) }
    [string] $BaseUri = $Uri.AbsoluteUri
    if ($Uri.AbsoluteUri -match ('^.+{0}' -f ([regex]::Escape($Uri.AbsolutePath)))) { $BaseUri = $Matches[0] }

    if ($ForceRefresh -or !$script:MsGraphMetadataCache.ContainsKey($BaseUri)) {
        #$MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop
        try {
            $script:MsGraphMetadataCache[$BaseUri] = Invoke-RestMethod -UseBasicParsing -Method Get -Uri $Uri -ErrorAction Ignore
        }
        catch {}
    }
    return $script:MsGraphMetadataCache[$BaseUri]
}

function Get-MsGraphEntityType {
    param (
        # Metadata URL for Microsoft Graph API.
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)]
        [uri] $Uri = 'https://graph.microsoft.com/v1.0/$metadata',
        # Name of endpoint.
        [Parameter(Mandatory = $false)]
        [string] $EntityName
    )

    process {
        $MsGraphMetadata = Get-MSGraphMetadata $Uri

        if (!$EntityName -and $Uri.Fragment -match '^#(.+?)(\(.+\))?(/\$entity)?$') { $EntityName = $Matches[1] }

        foreach ($Schema in $MsGraphMetadata.Edmx.DataServices.Schema) {
            foreach ($EntitySet in $Schema.EntityContainer.EntitySet) {
                if ($EntitySet.Name -eq $EntityName) {
                    return $EntitySet.EntityType
                }
            }
        }
    }
}

function Expand-MsGraphResult {
    param (
        # Results from MS Graph API.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object[]] $Results,
        # Do not expand result values
        [Parameter(Mandatory = $false)]
        [switch] $RawOutput,
        # Copy ODataContext to each result value
        [Parameter(Mandatory = $false)]
        [switch] $KeepODataContext,
        # Add ODataType to each result value
        [Parameter(Mandatory = $false)]
        [switch] $AddODataType
    )

    process {
        foreach ($Result in $Results) {
            if (!$RawOutput -and (Get-ObjectPropertyValue $Result.psobject.Properties 'Name') -contains 'value') {
                foreach ($ResultValue in $Result.value) {
                    if ($AddODataType) {
                        $ODataType = Get-ObjectPropertyValue $Result '@odata.context' | Get-MsGraphEntityType
                        if ($ODataType) { $ODataType = '#' + $ODataType }
                        if ($ResultValue -is [hashtable] -and !$ResultValue.ContainsKey('@odata.type')) {
                            $ResultValue.Add('@odata.type', $ODataType)
                        }
                        elseif ($ResultValue.psobject.Properties.Name -notcontains '@odata.type') {
                            $ResultValue | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value $ODataType
                        }
                    }
                    if ($KeepODataContext) {
                        if ($ResultValue -is [hashtable]) {
                            $ResultValue.Add('@odata.context', ('{0}/$entity' -f $Result.'@odata.context'))
                        }
                        else {
                            $ResultValue | Add-Member -MemberType NoteProperty -Name '@odata.context' -Value ('{0}/$entity' -f $Result.'@odata.context')
                        }
                    }
                    Write-Output $ResultValue
                }
            }
            else { Write-Output $Result }
        }
    }
}

function Get-MsGraphResultsCount {
    [CmdletBinding()]
    param (
        # Graph endpoint such as "users".
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [uri] $Uri,
        # API Version.
        [Parameter(Mandatory = $false)]
        [ValidateSet('v1.0', 'beta')]
        [string] $ApiVersion = 'v1.0',
        # Base URL for Microsoft Graph API.
        [Parameter(Mandatory = $false)]
        [uri] $GraphBaseUri = $script:mapMgEnvironmentToMgEndpoint[$script:ConnectState.CloudEnvironment]
    )

    process {
        if ($Uri.IsAbsoluteUri) {
            $uriEndpointCount = New-Object System.UriBuilder -ArgumentList $Uri -ErrorAction Stop
        }
        else {
            $uriEndpointCount = New-Object System.UriBuilder -ArgumentList $GraphBaseUri -ErrorAction Stop
            $uriEndpointCount.Path = ([IO.Path]::Combine($uriEndpointCount.Path, $ApiVersion, $Uri))
        }
        ## Remove $ref from path
        $uriEndpointCount.Path = $uriEndpointCount.Path -replace '/\$ref$', ''
        ## Add $count segment to path
        $uriEndpointCount.Path = ([IO.Path]::Combine($uriEndpointCount.Path, '$count'))
        ## $count is not supported with $expand parameter so remove it.
        [hashtable] $QueryParametersUpdated = ConvertFrom-QueryString $uriEndpointCount.Query -AsHashtable
        if ($QueryParametersUpdated.ContainsKey('$expand')) { $QueryParametersUpdated.Remove('$expand') }
        $uriEndpointCount.Query = ConvertTo-QueryString $QueryParametersUpdated
        $MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop
        [int] $Count = $null
        try {
            $Count = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing -Method Get -Uri $uriEndpointCount.Uri -Headers @{ ConsistencyLevel = 'eventual' } -ErrorAction Ignore
        }
        catch {}
        return $Count
    }
}

function Get-MsGraphResponseDetail {
    [CmdletBinding()]
    param (
        # ErrorRecord from exception or batch response object
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObject
    )

    process {
        $ResponseDetail = [ordered]@{}

        if ($InputObject -is [System.Management.Automation.ErrorRecord]) {
            if ($InputObject.Exception.psobject.Properties.Name.Contains('Response')) {
                ## Get Response Body
                if ($InputObject.ErrorDetails) {
                    $ResponseDetail['Response'] = '{0} {1} HTTP/{2}' -f $InputObject.Exception.Response.StatusCode.value__, $InputObject.Exception.Response.ReasonPhrase, $InputObject.Exception.Response.Version
                    $ResponseDetail['Content-Type'] = $InputObject.Exception.Response.Content.Headers.ContentType.ToString()
                    $ResponseDetail['Content'] = $InputObject.ErrorDetails.Message
                }
                elseif ($InputObject.Exception -is [System.Net.WebException]) {
                    if ($InputObject.Exception.Response) {
                        $ResponseDetail['Response'] = '{0} {1} HTTP/{2}' -f $InputObject.Exception.Response.StatusCode.value__, $InputObject.Exception.Response.StatusDescription, $InputObject.Exception.Response.ProtocolVersion
                        $ResponseDetail['Content-Type'] = $InputObject.Exception.Response.Headers.GetValues('Content-Type') -join '; '

                        $StreamReader = New-Object System.IO.StreamReader -ArgumentList $InputObject.Exception.Response.GetResponseStream()
                        try { $ResponseDetail['Content'] = $StreamReader.ReadToEnd() }
                        finally { $StreamReader.Close() }
                    }
                }

                $ResponseDetail['ContentParsed'] = $null
                if ($ResponseDetail['Content-Type'] -eq 'application/json') { $ResponseDetail['ContentParsed'] = ConvertFrom-Json $ResponseDetail['Content'] }
                $ResponseDetail['error-message'] = Get-ObjectPropertyValue $ResponseDetail['ContentParsed'] error message
                $ResponseDetail['Request'] = '{0} {1}' -f $InputObject.TargetObject.Method, $InputObject.TargetObject.RequestUri.AbsoluteUri
                if ($InputObject.Exception.Response) {
                    $ResponseDetail['Date'] = $InputObject.Exception.Response.Headers.GetValues('Date')[0]
                    $ResponseDetail['request-id'] = $InputObject.Exception.Response.Headers.GetValues('request-id')[0]
                    $ResponseDetail['client-request-id'] = $InputObject.Exception.Response.Headers.GetValues('client-request-id')[0]
                    try {
                        if ($InputObject.Exception.Response.Headers.GetValues('Retry-After')[0]) { $ResponseDetail['Retry-After'] = $InputObject.Exception.Response.Headers.GetValues('Retry-After')[0] }
                    }
                    catch {}
                }
            }
        }
        else {
            $ResponseDetail['Response'] = '{0} {1}' -f $InputObject.status, (Get-ObjectPropertyValue $InputObject body error code)
            $ResponseDetail['Content-Type'] = Get-ObjectPropertyValue $InputObject headers 'Content-Type'
            $ResponseDetail['ContentParsed'] = Get-ObjectPropertyValue $InputObject body
            $ResponseDetail['Content'] = ConvertTo-Json $ResponseDetail['ContentParsed'] -Depth 5 -Compress
            $ResponseDetail['error-message'] = Get-ObjectPropertyValue $InputObject body error message
            $ResponseDetail['Date'] = Get-ObjectPropertyValue $InputObject body error innerError 'date'
            $ResponseDetail['request-id'] = Get-ObjectPropertyValue $InputObject body error innerError 'request-id'
            $ResponseDetail['client-request-id'] = Get-ObjectPropertyValue $InputObject body error innerError 'client-request-id'
            if (Get-ObjectPropertyValue $InputObject headers 'Retry-After') { $ResponseDetail['Retry-After'] = Get-ObjectPropertyValue $InputObject headers 'Retry-After' }
        }

        Write-Output $ResponseDetail
    }
}

# SIG # Begin signature block
# MIInqgYJKoZIhvcNAQcCoIInmzCCJ5cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDPVlC3d/jVZQo4
# mY7fv2KK2hJnZBi432TA+yq+oSJWIqCCDYEwggX/MIID56ADAgECAhMzAAACzI61
# lqa90clOAAAAAALMMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NjAxWhcNMjMwNTExMjA0NjAxWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCiTbHs68bADvNud97NzcdP0zh0mRr4VpDv68KobjQFybVAuVgiINf9aG2zQtWK
# No6+2X2Ix65KGcBXuZyEi0oBUAAGnIe5O5q/Y0Ij0WwDyMWaVad2Te4r1Eic3HWH
# UfiiNjF0ETHKg3qa7DCyUqwsR9q5SaXuHlYCwM+m59Nl3jKnYnKLLfzhl13wImV9
# DF8N76ANkRyK6BYoc9I6hHF2MCTQYWbQ4fXgzKhgzj4zeabWgfu+ZJCiFLkogvc0
# RVb0x3DtyxMbl/3e45Eu+sn/x6EVwbJZVvtQYcmdGF1yAYht+JnNmWwAxL8MgHMz
# xEcoY1Q1JtstiY3+u3ulGMvhAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUiLhHjTKWzIqVIp+sM2rOHH11rfQw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDcwNTI5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAeA8D
# sOAHS53MTIHYu8bbXrO6yQtRD6JfyMWeXaLu3Nc8PDnFc1efYq/F3MGx/aiwNbcs
# J2MU7BKNWTP5JQVBA2GNIeR3mScXqnOsv1XqXPvZeISDVWLaBQzceItdIwgo6B13
# vxlkkSYMvB0Dr3Yw7/W9U4Wk5K/RDOnIGvmKqKi3AwyxlV1mpefy729FKaWT7edB
# d3I4+hldMY8sdfDPjWRtJzjMjXZs41OUOwtHccPazjjC7KndzvZHx/0VWL8n0NT/
# 404vftnXKifMZkS4p2sB3oK+6kCcsyWsgS/3eYGw1Fe4MOnin1RhgrW1rHPODJTG
# AUOmW4wc3Q6KKr2zve7sMDZe9tfylonPwhk971rX8qGw6LkrGFv31IJeJSe/aUbG
# dUDPkbrABbVvPElgoj5eP3REqx5jdfkQw7tOdWkhn0jDUh2uQen9Atj3RkJyHuR0
# GUsJVMWFJdkIO/gFwzoOGlHNsmxvpANV86/1qgb1oZXdrURpzJp53MsDaBY/pxOc
# J0Cvg6uWs3kQWgKk5aBzvsX95BzdItHTpVMtVPW4q41XEvbFmUP1n6oL5rdNdrTM
# j/HXMRk1KCksax1Vxo3qv+13cCsZAaQNaIAvt5LvkshZkDZIP//0Hnq7NnWeYR3z
# 4oFiw9N2n3bb9baQWuWPswG0Dq9YT9kb+Cs4qIIwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZfzCCGXsCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAsyOtZamvdHJTgAAAAACzDAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgle4vvYEZ
# gme3b13D9cXnH84daXzm+ctnO7Xin15g+xEwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQBFwx3KMX5vKPNGYQyqtQi5XKRM5YqTDwkf3Iujn+PY
# gxemTMGqJ2ekLkIioeQIZcgGfc1YfNvQPd5F019fgDlLQgSdmXTbdPTo9cjUa6sI
# ndC2lW1MY4FeAgDZZ7m2T5TCdKSYYhdTc7xh1gwfFYYP7u2EqJau0lmc6ejlVKz6
# tKK8cr6SMCrgVMIBBlbBcPhu6IzlY2i4GeshHa4AvwwXFEuf2zBAVk9ulowjOpRm
# k2D3NvGx2CwRoUv15Y0rI+vbO5F+GQye5L8Owgoj7cIEcYkKXm8rvVauVkeGFBVS
# 2NLj29B+UrqoC62+go9kIRMcKG4jnX5Otyap7D82Bk2SoYIXCTCCFwUGCisGAQQB
# gjcDAwExghb1MIIW8QYJKoZIhvcNAQcCoIIW4jCCFt4CAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEICvLaFDWrEr5B+ViH0Cv4WQTTMCx8+qeqErarHZ4
# mq3fAgZjTtXH5XQYEzIwMjIxMDI3MjE0NTA1LjE2OFowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjo0RDJGLUUzREQtQkVFRjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCEVwwggcQMIIE+KADAgECAhMzAAABsKHjgzLojTvAAAEA
# AAGwMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIyMDMwMjE4NTE0MloXDTIzMDUxMTE4NTE0Mlowgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0RDJG
# LUUzREQtQkVFRjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJzGbTsM19KCnQc5RC7V
# oglySXMKLut/yWWPQWD6VAlJgBexVKx2n1zgX3o/xA2ZgZ/NFGcgNDRCJ7mJiOeW
# 7xeHnoNXPlg7EjYWulfk3oOAj6a7O15GvckpYsvLcx+o8Se8CrfIb40EJ8W0Qx4T
# IXf0yDwAJ4/qO94dJ/hGabeJYg4Gp0G0uQmhwFovAWTHlD1ci+sp36AxT9wIhHqw
# /70tzMvrnDF7jmQjaVUPnjOgPOyFWZiVr7e6rkSl4anT1tLv23SWhXqMs14wolv4
# ZeQcWP84rV2Frr1KbwkIa0vlHjlv4xG9a6nlTRfo0CYUQDfrZOMXCI5KcAN2BZ6f
# Vb09qtCdsWdNNxB0y4lwMjnuNmx85FNfzPcMZjmwAF9aRUUMLHv626I67t1+dZoV
# PpKqfSNmGtVt9DETWkmDipnGg4+BdTplvgGVq9F3KZPDFHabxbLpSWfXW90MZXOu
# FH8yCMzDJNUzeyAqytFFyLZir3j4T1Gx7lReCOUPw1puVzbWKspV7ModZjtN/IUW
# dVIdk3HPp4QN1wwdVvdXOsYdhG8kgjGyAZID5or7C/75hyKQb5F0Z+Ee04uY9K+s
# DZ3l3z8TQZWAfYurbZCMWWnmJVsu5V4PR5PO+U6D7tAtMvMULNYibT9+sxVZK/WQ
# er2JJ9q3Z7ljFs4lgpmfc6AVAgMBAAGjggE2MIIBMjAdBgNVHQ4EFgQUOt8BJDcB
# Jm4dy6ASZHrXIEfWNj8wHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIw
# XwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3Js
# MGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3Nv
# ZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENB
# JTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcD
# CDANBgkqhkiG9w0BAQsFAAOCAgEA3XPih5sNtUfAyLnlXq6MZSpCh0TF+uG+nhIJ
# 44//cMcQGEViZ2N263NwvrQjCFOni/+oxf76jcmUhcKWLXk9hhd7vfFBhZZzcF5a
# Ns07Uligs24pveasFuhmJ4y82OYm1G1ORYsFndZdvF//NrYGxaXqUNlRHQlskV/p
# mccqO3Oi6wLHcPB1/WRTLJtYbIiiwE/uTFEFEL45wWD/1mTCPEkFX3hliXEypxXz
# dZ1k6XqGTysGAtLXUB7IC6CH26YygKQuXG8QjcJBAUG/9F3yNZOdbFvn7FinZyNc
# IVLxld7h0bELfQzhIjelj+5sBKhLcaFU0vbjbmf0WENgFmnyJNiMrL7/2FYOLsgi
# QDbJx6Dpy1EfvuRGsdL5f+jVVds5oMaKrhxgV7oEobrA6Z56nnWYN47swwouucHf
# 0ym1DQWHy2DHOFRRN7yv++zes0GSCOjRRYPK7rr1Qc+O3nsd604Ogm5nR9QqhOOc
# 2OQTrvtSgXBStu5vF6W8DPcsns53cQ4gdcR1Y9Ng5IYEwxCZzzYsq9oalxlH+ZH/
# A6J7ZMeSNKNkrXPx6ppFXUxHuC3k4mzVyZNGWP/ZgcUOi2qV03m6Imytvi1kfGe6
# YdCh32POgWeNH9lfKt+d1M+q4IhJLmX0E2ZZICYEb9Q0romeMX8GZ+cbhuNsFimJ
# ga/fjjswggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw
# MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx
# MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/
# XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1
# hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7
# M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K
# Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy
# 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80
# 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc
# NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha
# YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL
# iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV
# 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG
# CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp
# zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT
# MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a
# GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br
# aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG
# AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN
# AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1
# OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA
# A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz
# aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L
# GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m
# Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0
# SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko
# JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm
# PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482
# 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7
# vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYICzzCC
# AjgCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNv
# MSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0RDJGLUUzREQtQkVFRjElMCMGA1UE
# AxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUA
# Ap4vkN3fD5FNBVYZklZeS/JFPBiggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt
# cCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOcFMUEwIhgPMjAyMjEwMjcyMDMz
# MDVaGA8yMDIyMTAyODIwMzMwNVowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA5wUx
# QQIBADAHAgEAAgIPBzAHAgEAAgISHzAKAgUA5waCwQIBADA2BgorBgEEAYRZCgQC
# MSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqG
# SIb3DQEBBQUAA4GBAH5I7HBZgxv+pNkzPe11C1tDhcpYJKgBqAFZMhvJ7A5ol9G/
# n/ytDmElvpvuhH1kCRi01s5L64XEkozoYCD/9HtsJp3t9kqmU56z7zKxJtQtHRuW
# wNW1OXXRGK9m0TFqj919JAaHw4Shi6YuFRb9Xdx+penFg631mDZqXw84NM2CMYIE
# DTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAGw
# oeODMuiNO8AAAQAAAbAwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzEN
# BgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgVPn+qoq4Ng37QCjxZqV4+Q0S
# AcsTlOIavmaVLSYL4AEwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDNBgtD
# d8uf9KTjGf1G67IfKmcNFJmeWTd6ilAy5xWEoDCBmDCBgKR+MHwxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABsKHjgzLojTvAAAEAAAGwMCIEII3a6VLe
# 1CMC40tmVjjlEXXdaONZatL9+R88XRxGvS/gMA0GCSqGSIb3DQEBCwUABIICAJIa
# 8uDzmkoxlpQbvG9dczx2OASW7LvAAFONzcmfkPqpMmt4GksZkAhe/p95TwaS12UI
# CPeZwW1PcjSATzUeQkE8TV5LQ1kchwPvTWVdvvcTdusgou1x2laTjBk11a12E7ZO
# 4y4TMQ560138RWHZxvWzaJjBMvtjci80lNy8IrQxqR2Tfyz/3zlH8pfQl1v5s3NZ
# UYncpTQCjs9S2saQBNsiXhFUOgGLZKZjcYzFWu/dYV4+cUQIyut7g4W3pYW9zMPj
# 8xhzdfF7npQzE/2cg/20P6e0qeSi8g8z45xawuuwBoZz7NssptWl5W2OWg5r01e7
# u4wY83e7GjsZ3iAGh9a9wVvkZFN/RBA9gGZBkRSwgFCssGkFPFyhOe3szHS6uUum
# JEqIGeRvfDzIYqekPQBQQvRVOc7IY8eAlbdqszuKuNTs8T30PRmRnbHMwso0vdhS
# 0FHDcLJYhvgAdmhC3uMyjNPXCpl49zdQ3Sy4EAFrYha4q8Bkh6RchWVgbZs7elc+
# j/QYaLj1MHdvmOs+UAqeIFPc8zHL9yfrNIHovg9i3BykZQU0FWVueReh0KQLJIp8
# yQ6MAmo+enjrzbUi/WLsf0RNhwWqFjahUS27PsjCKjNxmaAeigzwxNt9oKxE3fMD
# JxC2fCBRBHr4gJCVL5Hf0dOKCkoGP4OtwMtpP4+I
# SIG # End signature block