0

I am using a custom function that imports data using an API. The function requires several different parameters, which I am currently defining with one $Params hashtable. From there I am adding other parameters that may or may not be added based on certain conditions. I have noticed that storing all parameters in the hashtable is not working properly, but will only work if particularly specified in the function call.

A basic version of the code that does not currently work is:

$ExtraParam = 'ccc'
$Params = @{
   Par1 = 'aaa'
   Par2 = 'bbb'
}
$Params.Par3 = $ExtraParam
Import-APIData @Params

The API is returning the following error message: {"errorId":"error.request.invalidRepresentation.malformed","errorCode":0,"message":"Invalid parameter value was specified."}

However, if I run the following code it works perfectly fine:

$ExtraParam = 'abc'
$Params = @{
   Par1 = '123'
   Par2 = '456'
}
Import-APIData @Params -Par3 $ExtraParam

I have verified that the parameters coming into the function are all of the correct type. The custom function I am calling is proprietary so I cannot share the entire function, but I was just wondering if there was anything that would reformat the parameters in the hashtable as opposed to explicitly calling it in the function call. Given that the two chunks of code are essentially the same I am hoping someone can point out some fundamental difference that may cause the top portion to not work.

Per a request of a commenter, here is the actual function. The _CallAPI function at the bottom is what actually calls the API. I will not be able to share that, but using the $Params output right above it (commented out in the code) may hopefully be enough to troubleshoot.

function Update-ICUser {
<#
    .SYNOPSIS
        Updates user on a Gensys ICWS server.
    .PARAMETER ICSession
        string ICSession. ICSession to update a user.
    .PARAMETER Id
        string Id. Id of the user to update.
    .PARAMETER InputObject
        object InputObject. Full IC User is needed.
    .PARAMETER Argument
        string Argument. Any query string parameters associated with the Api call.
    .Example
        $ICSession = New-ICSession -ComputerName server1 -Credential $Credential
        Get-ICUser -Id user1 -Full |Update-ICUser -ICSession $ICsession -Extension 1234 
        Example 1: Update a user's extension.
#>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
            Position = 0)]
        [ICSession]$ICSession,
        [Parameter(Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'InputObject',
            Position = 1)]
        [object]$InputObject,
        [Parameter(Mandatory,
            ParameterSetName = 'Manual',
            Position = 2)]
        [string]$Id,
        [Parameter(ParameterSetName = 'Manual',
            Position = 3)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$DisplayName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 4)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$OutboundAni,
        [Parameter(ParameterSetName = 'Manual',
            Position = 5)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Title,
        [Parameter(ParameterSetName = 'Manual',
            Position = 6)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$OfficeLocation,
        [Parameter(ParameterSetName = 'Manual',
            Position = 7)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Notes,
        [Parameter(ParameterSetName = 'Manual',
            Position = 8)]
        [Parameter(ParameterSetName = 'InputObject')]
        [int]$Cost = 0,
        [Parameter(ParameterSetName = 'Manual',
            Position = 9)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$AutoAnswerAcdInteractions = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 10)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$AutoAnswerNonAcdInteractions = $true,
        [Parameter(ParameterSetName = 'Manual',
            Position = 11)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$ApplicationId = 'InteractionDesktop',
        [Parameter(ParameterSetName = 'Manual',
            Position = 12)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$ExcludeFromDirectory = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 13)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Extension,
        [Parameter(ParameterSetName = 'Manual',
            Position = 14)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$FaxCapability = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 15)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$OutlookIntegrationEnabled = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 16)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$FirstName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 17)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$LastName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 18)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string[]]$WorkgroupList,
        [Parameter(ParameterSetName = 'Manual',
            Position = 19)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$DepartmentName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 20)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$City,
        [Parameter(ParameterSetName = 'Manual',
            Position = 21)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$PostalCode,
        [Parameter(ParameterSetName = 'Manual',
            Position = 22)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$State,
        [Parameter(ParameterSetName = 'Manual',
            Position = 23)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$StreetAddress,
        [Parameter(ParameterSetName = 'Manual',
            Position = 24)]
        [Parameter(ParameterSetName = 'InputObject')]
        [int]$HomeSite,
        [Parameter(ParameterSetName = 'Manual',
            Position = 25)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string[]]$LicenseList,
        [Parameter(ParameterSetName = 'Manual',
            Position = 26)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string[]]$RoleList
    )
    Try {
        if ($InputObject) {
            $User = $InputObject
            $Id = $InputObject.configurationId.id
        }
        else {
            $User = Get-ICUser -ICSession $ICSession -Id $Id -Full
        }
        'Id','InputObject','ICSession' |Foreach-Object {
            $PSBoundParameters.Remove($PSItem) |Out-Null
        }
        $PSBoundParameters.GetEnumerator() |Foreach-Object {
            if ($PSItem.Key -in ('Title','DepartmentName','City','State','StreetAddress','FirstName','LastName','Notes')) {
                if ($PSItem.Key -in ('State','FirstName','LastName')) {
                    $Key = switch ($PSItem.Key) {
                        'State' {'stateOrProvince'}
                        'FirstName' {'givenName'}
                        'LastName' {'surName'}
                    }
                }
                else {
                    $Key = $PSItem.Key
                }
                $User.personalInformationProperties.$Key = $PSItem.Value
            }
            elseif ($PSItem.Key -in ('WorkgroupList','LicenseList','RoleList')) {
                $Key = $PSItem.Key
                if ($Key = 'WorkgroupList' -and $WorkgroupList) {
                    [array]$Value = Get-ICWorkgroup -ICSession $SessionKey -Id $WorkgroupList
                    $User.Workgroups = $Value
                }
                if ($Key = 'LicenseList' -and $LicenseList) {
                    $Value = @((Get-ICLicenseAllocation -ICSession $SessionKey -Id $LicenseList).configurationId)
                    if ($User.licenseProperties.PSobject.Properties.name -match 'additionalLicenses') {
                        $User.licenseProperties.additionalLicenses = $Value
                    }
                    else {
                        $User.licenseProperties | Add-Member -MemberType NoteProperty -Name 'additionalLicenses' -Value $Value -Force
                    }
                }
                if ($Key = 'RoleList' -and $RoleList) {
                    [array]$Value = Get-ICRole -ICSession $SessionKey -Id $RoleList
                    if ($User.roles.PSobject.Properties.name -match 'actualValue') {
                        $User.Roles.actualValue = $Value
                    }
                    else {
                        $User.roles | Add-Member -MemberType NoteProperty -Name 'actualValue' -Value $Value -Force
                    }
                    
                }
            }
            else {
                $Key = $PSItem.Key
                $Value = $PSItem.Value
                $User |Add-Member -MemberType NoteProperty -Name $Key -Value $Value -Force
            }
        }
        $Body = $User |ConvertTo-Json -Depth 5
        $Params = @{
            Area = $Configuration.Area.Configuration
            Resource = $Configuration.Resource.Users
            ICSession = $ICSession
            Id = $Id
            Method = 'Put'
            ContentType = 'application/json'
            Body = $Body
        }
        # $Params
        _CallApi @Params
    }
    Catch {
        _ExceptionError
    }
}

I will also provide everything on the console. In this example the $AutoAnswerAcd parameter is the one that is failing, but I have gotten it to fail and work by adding/removing other parameters. I have not detected a pattern that makes it fail yet, other than that it always works outside of the splat. You can see this pattern below.

PS C:\> $SessionKey = New-ICSession -ComputerName $server -Credential $Credential
PS C:\> $Params = @{
>>     ICSession = $SessionKey
>>     Id = 'tautomation'
>>     WorkgroupList = "MultiSite-UserSpan"
>>     RoleList = "Business User"
>>     LicenseList = "I3_ACCESS_RECORDER"
>>     DepartmentName = "Infr & Ops"
>>     Title = "Process Automation Engineer"
>>     OfficeLocation = "Remote"
>>     AutoAnswerAcdInteractions = $true
>> }
PS C:\> Update-ICUser @Params
Exception: C:\Repos\ICTools\Code\Private\_ExceptionError.ps1:10
Line |
  10 |          throw "$($Exception)`n$($Exception.Exception.Message)"
     |          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | {"errorId":"error.request.invalidRepresentation.malformed","errorCode":0,"message":"Invalid parameter value was specified."} Response status code does not indicate
     | success: 400 (Bad Request). {"errorId":"error.request.invalidRepresentation.malformed","errorCode":0,"message":"Invalid parameter value was specified."} Response status
     | code does not indicate success: 400 (Bad Request).

PS C:\> $Params = @{
>>     ICSession = $SessionKey
>>     Id = 'tautomation'
>>     WorkgroupList = "MultiSite-UserSpan"
>>     RoleList = "Business User"
>>     LicenseList = "I3_ACCESS_RECORDER"
>>     DepartmentName = "Infr & Ops"
>>     Title = "Process Automation Engineer"
>>     OfficeLocation = "Remote"
>> }
PS C:\> Update-ICUser @Params

id          uri
--          ---
tautomation /configuration/users/tautomation

PS C:\> Update-ICUser @Params -AutoAnswerAcdInteractions $true

id          uri
--          ---
tautomation /configuration/users/tautomation

PS C:\>

Thanks!

Andrew Draper
  • 101
  • 1
  • 9
  • 3
    In the 1st one you are doing aaa, bbb, and ccc. In the second, 123, 456 and abc The error says "Invalid parameter VALUE was specified". Could it maybe be that a value you are using in the first example just is not valid or have you tried with same values for both scenarios? – Daniel Aug 24 '22 at 20:08
  • The code I provided is not real, but merely a sample of what is going on. The values should not matter, as they are all strings. – Andrew Draper Aug 24 '22 at 20:39
  • 3
    Can you post a dummy version of ```Import-APIData``` that just contains the signature? E.g. ```function Import-APIData { params( $Par1, $Par2, $Par3 ) }```, and confirm you still see the same issue when calling that dummy function instead of the real one? – mclayton Aug 24 '22 at 20:55
  • 1
    It might be that `Import-APIData` depends on the ordering of arguments (e. g. by using the `$args` array or using `ValueFromRemainingArguments` attribute). Hashtables enumerate in random order, try to use an ordered dictionary instead: `$Params = [ordered] @{ ... }` – zett42 Aug 24 '22 at 20:57
  • There is no issue with both examples, unless you share with us this `Import-APIData` function we would be just guessing. Also splatting will bind by name so don't think the hashtable order matters here – Santiago Squarzon Aug 24 '22 at 21:30
  • Per several requests, I have provided the signature of the real function, Update-ICUser – Andrew Draper Aug 24 '22 at 21:36
  • Does the function enumerate `$PSBoundParameters`, e. g. `foreach( $arg in $PSBoundParameters.GetEnumerator() ) { ... }` ? – zett42 Aug 24 '22 at 22:04
  • 2
    At some point you are doing a POST request correct? Have you inspected the body being sent in both scenarios and if so, are they different in any way? – Daniel Aug 24 '22 at 22:56
  • As others have commented, your code might be relying on the order of keys in ```$PSBoundParameters```. What happens if you add the splat *after* the named parameter? I.e. ```Import-APIData -Par3 $ExtraParam @Params``` (note there’s no *guarantee* of order since ```$PSBoundParameters``` inherits from ```Dictionary``` but in my tests there was *some* predictability) – mclayton Aug 25 '22 at 04:13
  • @zett42: Yes, later in the function there is a `$PSBoundParameters.GetEnumerator() |Foreach-Object {` loop because not every parametername for the function will match up with the object name in the API (givenName v. FirstName, for example) – Andrew Draper Aug 25 '22 at 12:38
  • @Daniel: That is a good idea. I used both $Params values from my console output shown above. The one with `AutoAnswerAcdInteractions` inside $Params (which failes) has all data like this: "OfficeLocation": [ "Remote" ], "AutoAnswerAcdInteractions": [ true ] While the second one with it outside $Params (which succeeds) looks like: "AutoAnswerAcdInteractions": true, "OfficeLocation": "Remote" . Both of these are at the end of the Body – Andrew Draper Aug 25 '22 at 12:40
  • @mclayton I tried using the extra variable before `@Params` in the function call, and it still works as expected – Andrew Draper Aug 25 '22 at 12:44
  • As an aside: it's best to avoid `[bool]` parameters in favor of `[switch]` parameters (splatting works the same). – mklement0 Aug 25 '22 at 12:48
  • 2
    Assuming that you're talking about the converted-to-JSON body when you say `"OfficeLocation": [ "Remote" ]`, this would imply that the values are wrapped in an _array_, but it's not clear from your code how that could happen. – mklement0 Aug 25 '22 at 12:55
  • 1
    It looks like the problem is how you build the http request - you appear to build different requests depending on how you invoke the function. Are you able to reduce your ```Update-ICUser``` function to an MRE (see https://stackoverflow.com/help/minimal-reproducible-example) that demonstrates the problem without having to include your proprietary code? – mclayton Aug 25 '22 at 13:07
  • I have provided the entire function for `New-ICUser`. I am not sure how much it will help though, since the core of the API call is all done in other functions. My predecessor created an entire module for this API, but he unfortunately left. – Andrew Draper Aug 25 '22 at 13:18
  • So to debug it further, I think you need to add some more logging - maybe something like this as the first line of your ```foreach-object``` - ```write-host "processing key '$($PSItem.Key)'"; write-host "user before = '$($User |ConvertTo-Json -Depth 5)'";```, and a similar ```write-host "user after = "``` at the end of the loop. There's a few things to watch - e.g. does ```Get-ICUSer``` even return a valid user to start with (i.e. ```AutoAnswerAcdInteractions``` is ```$true``` instead of ```[$true]```), and does ```$body``` look valid before you call ```_CallApi @Params```? – mclayton Aug 25 '22 at 13:35
  • @mclayton I posted the Body in a previous comment to Daniel above. Using the parameter inside of `@Params` yielded a list, while doing it outside yielded a single value – Andrew Draper Aug 25 '22 at 13:38
  • Yep, so you want to try to find out at what point they diverge - e.g. is the ```$User``` different at the *start* of the function for the two different calls, or does something in your code update it differently somehow? If you convert your ```$User``` variable to json at various points in your code and log the value out you can see if and when they diverge, and that might give a clue to how to fix it... – mclayton Aug 25 '22 at 13:46
  • @mclayton I did a good amount of testing. Since each body is over 1000 lines I used `set-clipboard` and then searched for the fields I wanted. I also added an extra paramter, `outboundAni`, which is actually the one I created tis forum for (its just a phone number). When `$Params` is all 10 parameters, it imports both outboundAni and the ACD field as a list when the loop iterates on the respective key. However if I exclude a few other parameters they get imported properly. The tl;dr: they are automatically being converted a list, but only if they are with several other parameters in $Params – Andrew Draper Aug 25 '22 at 14:13
  • To clarify, with $Params having 5 values, the final Body json looks like `"AutoAnswerAcdInteractions": false, "OutboundAni": "8675309"`, and when $Params has 10 values they look like `"OutboundAni": [ "8675309" ], "AutoAnswerAcdInteractions": [ false ]`, where they get turned into a list when the loop goes over that key – Andrew Draper Aug 25 '22 at 14:17

1 Answers1

4

Ok, so think I've found what's happening.

Firstly, here's an instrumented version of your code that gives some log output as it runs - that'll help see where it's going wrong...

function Update-ICUser {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
            Position = 0)]
        [string]$ICSession,
        [Parameter(Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'InputObject',
            Position = 1)]
        [object]$InputObject,
        [Parameter(Mandatory,
            ParameterSetName = 'Manual',
            Position = 2)]
        [string]$Id,
        [Parameter(ParameterSetName = 'Manual',
            Position = 3)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$DisplayName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 4)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$OutboundAni,
        [Parameter(ParameterSetName = 'Manual',
            Position = 5)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Title,
        [Parameter(ParameterSetName = 'Manual',
            Position = 6)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$OfficeLocation,
        [Parameter(ParameterSetName = 'Manual',
            Position = 7)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Notes,
        [Parameter(ParameterSetName = 'Manual',
            Position = 8)]
        [Parameter(ParameterSetName = 'InputObject')]
        [int]$Cost = 0,
        [Parameter(ParameterSetName = 'Manual',
            Position = 9)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$AutoAnswerAcdInteractions = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 10)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$AutoAnswerNonAcdInteractions = $true,
        [Parameter(ParameterSetName = 'Manual',
            Position = 11)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$ApplicationId = 'InteractionDesktop',
        [Parameter(ParameterSetName = 'Manual',
            Position = 12)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$ExcludeFromDirectory = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 13)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Extension,
        [Parameter(ParameterSetName = 'Manual',
            Position = 14)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$FaxCapability = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 15)]
        [Parameter(ParameterSetName = 'InputObject')]
        [bool]$OutlookIntegrationEnabled = $false,
        [Parameter(ParameterSetName = 'Manual',
            Position = 16)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$FirstName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 17)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$LastName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 18)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string[]]$WorkgroupList,
        [Parameter(ParameterSetName = 'Manual',
            Position = 19)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$DepartmentName,
        [Parameter(ParameterSetName = 'Manual',
            Position = 20)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$City,
        [Parameter(ParameterSetName = 'Manual',
            Position = 21)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$PostalCode,
        [Parameter(ParameterSetName = 'Manual',
            Position = 22)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$State,
        [Parameter(ParameterSetName = 'Manual',
            Position = 23)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$StreetAddress,
        [Parameter(ParameterSetName = 'Manual',
            Position = 24)]
        [Parameter(ParameterSetName = 'InputObject')]
        [int]$HomeSite,
        [Parameter(ParameterSetName = 'Manual',
            Position = 25)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string[]]$LicenseList,
        [Parameter(ParameterSetName = 'Manual',
            Position = 26)]
        [Parameter(ParameterSetName = 'InputObject')]
        [string[]]$RoleList
    )

    if ($InputObject) {
        $User = $InputObject
        $Id = $InputObject.configurationId.id
    }
    else {
        $User = [pscustomobject] @{
            personalInformationProperties = [pscustomobject] @{
                DepartmentName = $null
                Title          = $null
            }
            licenseProperties = [pscustomobject] @{}
            roles             = [pscustomobject] @{}
            Workgroups        = $null
        }
        #Get-ICUser -ICSession $ICSession -Id $Id -Full
    }

    #write-host "user = '$($User |ConvertTo-Json -Depth 5)'"

    'Id','InputObject','ICSession' |Foreach-Object {
        $PSBoundParameters.Remove($PSItem) |Out-Null
    }

    $PSBoundParameters.GetEnumerator() |Foreach-Object {
        write-host "processing key '$($PSItem.Key)'"

        if ($PSItem.Key -in ('Title','DepartmentName','City','State','StreetAddress','FirstName','LastName','Notes')) {
            if ($PSItem.Key -in ('State','FirstName','LastName')) {
                $Key = switch ($PSItem.Key) {
                    'State' {'stateOrProvince'}
                    'FirstName' {'givenName'}
                    'LastName' {'surName'}
                }
            }
            else {
                $Key = $PSItem.Key
            }
            $User.personalInformationProperties.$Key = $PSItem.Value
        }
        elseif ($PSItem.Key -in ('WorkgroupList','LicenseList','RoleList')) {
            $Key = $PSItem.Key
            if ($Key = 'WorkgroupList' -and $WorkgroupList) {
                write-host "    WorkgroupList - casting to array"
                #$Value = @( Get-ICWorkgroup -ICSession $SessionKey -Id $WorkgroupList )
                [array] $Value = @()
                #$User.Workgroups = $Value
            }
            if ($Key = 'LicenseList' -and $LicenseList) {
                write-host "    LicenseList - casting to array"
                #$Value = @((Get-ICLicenseAllocation -ICSession $SessionKey -Id $LicenseList).configurationId)
                $Value = @()
                if ($User.licenseProperties.PSobject.Properties.name -match 'additionalLicenses') {
                    $User.licenseProperties.additionalLicenses = $Value
                }
                else {
                    $User.licenseProperties | Add-Member -MemberType NoteProperty -Name 'additionalLicenses' -Value $Value -Force
                }
            }
            if ($Key = 'RoleList' -and $RoleList) {
                write-host "    RoleList - casting to array"
                #[array]$Value = Get-ICRole -ICSession $SessionKey -Id $RoleList
                [array]$Value = @()
                if ($User.roles.PSobject.Properties.name -match 'actualValue') {
                    $User.Roles.actualValue = $Value
                }
                else {
                    $User.roles | Add-Member -MemberType NoteProperty -Name 'actualValue' -Value $Value -Force
                }
                
            }
        }
        else {
            write-host "adding key '$($PSItem.Key)' with value '$($PSItem.Value)'"
            write-host "    value before is '$($Value.GetType().FullName)'"
            $Key = $PSItem.Key
            write-host "    value after is '$($Value.GetType().FullName)'"
            $Value = $PSItem.Value
            #write-host ($PSItem.Key | convertto-json)
            #write-host ($PSItem.Value | convertto-json)
            #write-host $PSItem.Key.GetType().FullNane
            #write-host $PSItem.Value.GetType().FullNane
            #write-host "user before = '$($User |ConvertTo-Json -Depth 5)'"
            $User |Add-Member -MemberType NoteProperty -Name $Key -Value $Value -Force
            #write-host "user after = '$($User |ConvertTo-Json -Depth 5)'"
        }


    }
    $Body = $User |ConvertTo-Json -Depth 5

    write-host $Body

}

and when you call it with, e.g. this:

$Params = @{
    ICSession = "aaa"
    Id = 'tautomation'
    WorkgroupList = "MultiSite-UserSpan"
    RoleList = "Business User"
    LicenseList = "I3_ACCESS_RECORDER"
    DepartmentName = "Infr & Ops"
    Title = "Process Automation Engineer"
    OfficeLocation = "Remote"
    AutoAnswerAcdInteractions = $true
}

Update-ICUser @Params

you get this output:

processing key 'DepartmentName'
processing key 'OfficeLocation'
adding key 'OfficeLocation' with value 'Remote'
processing key 'Title'
processing key 'WorkgroupList'
    WorkgroupList - casting to array
    LicenseList - casting to array
    RoleList - casting to array
processing key 'AutoAnswerAcdInteractions'
adding key 'AutoAnswerAcdInteractions' with value 'True'
    value before is 'System.Object[]'
    value after is 'System.Object[]'
processing key 'RoleList'
    WorkgroupList - casting to array
    LicenseList - casting to array
    RoleList - casting to array
processing key 'LicenseList'
    WorkgroupList - casting to array
    LicenseList - casting to array
    RoleList - casting to array
{
  "personalInformationProperties": {
    "DepartmentName": "Infr & Ops",
    "Title": "Process Automation Engineer"
  },
  "licenseProperties": {
    "additionalLicenses": []
  },
  "roles": {
    "actualValue": []
  },
  "Workgroups": null,
  "OfficeLocation": "Remote",
  "AutoAnswerAcdInteractions": [
    true
  ]
}

So there's a few things to note:

  • casting to array

    casting to array seems to be getting logged for three parameters (WorkgroupList, RoleList and LicenseList) when processing each of them.

    If you look, your code is doing this: if ($Key = 'WorkgroupList' -and $WorkgroupList) but = is an assignment operator, not a comparison operator, so what you really want is if ($Key -eq 'WorkgroupList' -and $WorkgroupList)

    It's the same for if ($Key = 'LicenseList' -and $LicenseList) and if ($Key = 'RoleList' -and $RoleList) - change those to -eq as well and that's one bug stomped...

  • adding key 'AutoAnswerAcdInteractions' with value 'True'

    The $Value variable is of type System.Object[] before and after assigning the parameter value. If you look at the previous parameter in the logs you'll see WorkgroupList - casting to array - this is changing the type of $Value to be an array. Any future attempts to assign values will automatically convert the value to an array, so $true becomes [ $true ], which is where your problem is coming from.

    Note the same cast is happening in RoleList, but not LicenseList.

    Since the order of keys is not guaranteed in a hashtable or $PSBoundParameters, your issue only happens if:

    • $WorkgroupList or $RoleList are specified in the function call
    • They happen to be enumerated before $AutoAnswerAcdInteractions

    To fix this, refactor WorkgroupList and RoleList to be the same as LicenseList - i.e. instead of:

    [array]$Value = <expression>
    

    use the Array subexpression operator:

    $Value = @( <expression> )
    

    This will ensure the value stored in the variable is an array without changing the type of the variable for future parameters.


Note that the reason the extra parameter appears to work is it changes the order of the items in $PSBoundParameters. For example:

$Params = @{
    ICSession = "aaa"
    Id = 'tautomation'
    WorkgroupList = "MultiSite-UserSpan"
    RoleList = "Business User"
    LicenseList = "I3_ACCESS_RECORDER"
    DepartmentName = "Infr & Ops"
    Title = "Process Automation Engineer"
    OfficeLocation = "Remote"
}

Update-ICUser @Params -AutoAnswerAcdInteractions $true

gives this output

processing key 'AutoAnswerAcdInteractions'
adding key 'AutoAnswerAcdInteractions' with value 'True'
processing key 'DepartmentName'
processing key 'Title'
processing key 'WorkgroupList'
    WorkgroupList - casting to array
processing key 'RoleList'
    RoleList - casting to array
processing key 'LicenseList'
    LicenseList - casting to array
processing key 'OfficeLocation'
adding key 'OfficeLocation' with value 'Remote'
    value before is 'System.Object[]'
    value after is 'System.Object[]'
{
  "personalInformationProperties": {
    "DepartmentName": "Infr & Ops",
    "Title": "Process Automation Engineer"
  },
  "licenseProperties": {
    "additionalLicenses": []
  },
  "roles": {
    "actualValue": []
  },
  "Workgroups": null,
  "AutoAnswerAcdInteractions": true,
  "OfficeLocation": [
    "Remote"
  ]
}

Note that AutoAnswerAcdInteractions is processed before any of the paths that change the type of $Value to an array.

Incidentally, the code also works if you omit WorkgroupList and RoleList because $Value never gets changed to an array:

$Params = @{
    ICSession = "aaa"
    Id = 'tautomation'
    #WorkgroupList = "MultiSite-UserSpan"
    #RoleList = "Business User"
    LicenseList = "I3_ACCESS_RECORDER"
    DepartmentName = "Infr & Ops"
    Title = "Process Automation Engineer"
    OfficeLocation = "Remote"
    AutoAnswerAcdInteractions = $true
}

Update-ICUser @Params

with the output:

processing key 'OfficeLocation'
adding key 'OfficeLocation' with value 'Remote'
processing key 'Title'
processing key 'AutoAnswerAcdInteractions'
adding key 'AutoAnswerAcdInteractions' with value 'True'
    value before is 'System.String'
    value after is 'System.String'
processing key 'DepartmentName'
processing key 'LicenseList'
    LicenseList - casting to array
{
  "personalInformationProperties": {
    "DepartmentName": "Infr & Ops",
    "Title": "Process Automation Engineer"
  },
  "licenseProperties": {
    "additionalLicenses": []
  },
  "roles": {},
  "Workgroups": null,
  "OfficeLocation": "Remote",
  "AutoAnswerAcdInteractions": true
}
mclayton
  • 8,025
  • 2
  • 21
  • 26
  • 1
    mclayton, you are a legend. I can't believe I missed that. Its always the simple things. Thank you so much for finding the solution! – Andrew Draper Aug 25 '22 at 15:52
  • No worries. My mantra for debugging scripts is usually "needs more write-host" - not everyone agrees with that approach, but it works for me :-) – mclayton Aug 25 '22 at 16:26