WorkItem/WorkItem.ps1

<#
.SYNOPSIS
Creates a copy of a work item, optionally changing its type
 
.DESCRIPTION
Use this cmdlet to create a copy of a work item (using its latest saved state/revision data) that is of the specified work item type. By default, the copy retains the same type of the original work item, unless the Type argument is specified
 
.PARAMETER WorkItem
Specifies the work item to be copied. Can be either a work item ID or an instance of Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem
 
.PARAMETER Type
Specifies the new type for the copy of the original work item. It can be provided as either a string representing the work item type name (e.g. "Bug" or "Task") or an instance of Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemType. When an instance of WorkItemType is provided, the team project from where that WorkItemType object was retrieved will be used to define where to copy the work item into, unless the Project argument is specified
 
.PARAMETER IncludeAttachments
Includes attachments as part of the copy process. By default, only field values are copied
 
.PARAMETER IncludeLinks
Includes work item links as part of the copy process. By default, only field values are copied
 
.PARAMETER Project
Specified the team project where the work item will be copied into. If omitted, the copy will be created in the same team project of the source work item. The value provided to this argument takes precedence over both the source team project and the team project of an instance of WorkItemType provided to the Type argument
 
.PARAMETER Collection
Specifies either a URL/name of the Team Project Collection to connect to, or a previously initialized TfsTeamProjectCollection object.
 
When using a URL, it must be fully qualified. The format of this string is as follows:
 
http[s]://<ComputerName>:<Port>/[<TFS-vDir>/]<CollectionName>
 
Valid values for the Transport segment of the URI are HTTP and HTTPS. If you specify a connection URI with a Transport segment, but do not specify a port, the session is created with standards ports: 80 for HTTP and 443 for HTTPS.
 
To connect to a Team Project Collection by using its name, a TfsConfigurationServer object must be supplied either via -Server argument or via a previous call to the Connect-TfsConfigurationServer cmdlet.
 
For more details, see the Get-TfsTeamProjectCollection cmdlet.
 
.PARAMETER SkipSave
Leaves the new work item in a "dirty" (unsaved) state, by not calling its Save() method. It is useful for when subsequents changes need to be made to the work item object before saving it. In that case, it is up to the user to later invoke the Save() method on the new work item object to persist the copy.
 
.PARAMETER Passthru
Returns the results of the command. It takes one of the following values: Original (returns the original work item), Copy (returns the newly created work item copy) or None.
 
.INPUTS
Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem
System.Int32
 
.EXAMPLE
Copy-TfsWorkItem -WorkItem 123
Creates (and returns) a copy of a work item with ID #123
 
.EXAMPLE
Get-TfsWorkItem -Filter '[System.WorkItemType] = "Bug"' | Copy-TfsWorkItem -Type Task
Retrieves all work item of Bug type and copy them as Tasks
 
.LINK
https://msdn.microsoft.com/en-us/library/ff738070.aspx
#>

Function Copy-TfsWorkItem
{
    [CmdletBinding()]
    [OutputType('Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem')]
    Param
    (
        [Parameter(ValueFromPipeline=$true)]
        [Alias("id")]
        [ValidateNotNull()]
        [object]
        $WorkItem,

        [Parameter()]
        [object] 
        $Type,

        [Parameter()]
        [object] 
        $Project,

        [Parameter()]
        [switch] 
        $IncludeAttachments,

        [Parameter()]
        [switch] 
        $IncludeLinks,

        [Parameter()]
        [switch] 
        $SkipSave,

        [Parameter()]
        [ValidateSet('Original', 'Copy', 'None')]
        [string]
        $Passthru = 'Copy',

        [Parameter()]
        [object]
        $Collection
    )

    Process
    {
        $wi = Get-TfsWorkItem -WorkItem $WorkItem -Collection $Collection
        #$store = $wi.Store

        if($Type)
        {
            if ($Project)
            {
                $tp = $Project
            }
            else
            {
                $tp = $wi.Project
            }
            $witd = Get-TfsWorkItemType -Type $Type -Project $tp -Collection $wi.Store.TeamProjectCollection
        }
        else
        {
            $witd = $wi.Type
        }

        $flags = [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemCopyFlags]::None

        if ($IncludeAttachments)
        {
            $flags = $flags -bor [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemCopyFlags]::CopyFiles
        }

        if ($IncludeLinks)
        {
            $flags = $flags -bor [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemCopyFlags]::CopyLinks
        }

        $copy = $wi.Copy($witd, $flags)

        if(-not $SkipSave)
        {
            $copy.Save()
        }

        if ($Passthru -eq 'Original')
        {
            return $wi
        }
        
        if($Passthru -eq 'Copy')
        {
            return $copy
        }
    }
}
<#
 
.SYNOPSIS
    Gets the contents of one or more work items.
 
.PARAMETER Project
    Specifies either the name of the Team Project or a previously initialized Microsoft.TeamFoundation.WorkItemTracking.Client.Project object to connect to. If omitted, it defaults to the connection opened by Connect-TfsTeamProject (if any).
 
For more details, see the Get-TfsTeamProject cmdlet.
 
.PARAMETER Collection
    Specifies either a URL/name of the Team Project Collection to connect to, or a previously initialized TfsTeamProjectCollection object.
 
When using a URL, it must be fully qualified. The format of this string is as follows:
 
http[s]://<ComputerName>:<Port>/[<TFS-vDir>/]<CollectionName>
 
Valid values for the Transport segment of the URI are HTTP and HTTPS. If you specify a connection URI with a Transport segment, but do not specify a port, the session is created with standards ports: 80 for HTTP and 443 for HTTPS.
 
To connect to a Team Project Collection by using its name, a TfsConfigurationServer object must be supplied either via -Server argument or via a previous call to the Connect-TfsConfigurationServer cmdlet.
 
For more details, see the Get-TfsTeamProjectCollection cmdlet.
 
.INPUTS
    Microsoft.TeamFoundation.WorkItemTracking.Client.Project
    System.String
#>

Function Get-TfsWorkItem
{
    [CmdletBinding(DefaultParameterSetName="Query by text")]
    [OutputType('Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem')]
    Param
    (
        [Parameter(Position=0, Mandatory=$true, ParameterSetName="Query by revision")]
        [Parameter(Position=0, Mandatory=$true, ParameterSetName="Query by date")]
        [Alias("id")]
        [ValidateNotNull()]
        [object]
        $WorkItem,

        [Parameter(ParameterSetName="Query by revision")]
        [Alias("rev")]
        [int]
        $Revision,

        [Parameter(Mandatory=$true, ParameterSetName="Query by date")]
        [datetime]
        $AsOf,

        [Parameter(Mandatory=$true, ParameterSetName="Query by WIQL")]
        [Alias('WIQL')]
        [Alias('QueryText')]
        [Alias('SavedQuery')]
        [Alias('QueryPath')]
        [string]
        $Query,

        # [Parameter(Mandatory=$true, ParameterSetName="Query by filter")]
        # [string[]]
        # $Fields,

        [Parameter(Mandatory=$true, ParameterSetName="Query by filter")]
        [string]
        $Filter,

        [Parameter(Position=0, Mandatory=$true, ParameterSetName="Query by text")]
        [string]
        $Text,

        [Parameter()]
        [hashtable]
        $Macros,

        [Parameter(ValueFromPipeline=$true)]
        [object]
        $Project,

        [Parameter()]
        [object]
        $Collection
    )

    Begin
    {
        #_ImportRequiredAssembly -AssemblyName 'Microsoft.TeamFoundation.WorkItemTracking.Client'
    }

    Process
    {
        if ($Project)
        {
            $tp = Get-TfsTeamProject -Project $Project -Collection $Collection
            $tpc = $tp.Store.TeamProjectCollection
            $store = $tp.Store
        }
        else
        {
            $tpc = Get-TfsTeamProjectCollection -Collection $Collection
            $store = $tpc.GetService([type]'Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore')
        }

        if ($WorkItem -is [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem])
        {
            if ((-Not $Revision) -and (-Not $AsOf))
            {
                return $WorkItem
            }
        }

        switch($PSCmdlet.ParameterSetName)
        {
            "Query by revision" {
                return _GetWorkItemByRevision $WorkItem $Revision $store
            }

            "Query by date" {
                return _GetWorkItemByDate $WorkItem $AsOf $store
            }

            "Query by text" {
                $localMacros = @{TfsQueryText=$Text}
                $Wiql = "SELECT * FROM WorkItems WHERE [System.Title] CONTAINS @TfsQueryText OR [System.Description] CONTAINS @TfsQueryText"
                return _GetWorkItemByWiql $Wiql $localMacros $tp $store 
            }

            "Query by filter" {
                $Wiql = "SELECT * FROM WorkItems WHERE $Filter"
                return _GetWorkItemByWiql $Wiql $Macros $tp $store 
            }

            "Query by WIQL" {
                _Log "Get-TfsWorkItem: Running query by WIQL. Query: $Query"
                return _GetWorkItemByWiql $Query $Macros $tp $store 
            }

            "Query by saved query" {
                return _GetWorkItemBySavedQuery $StoredQueryPath $Macros $tp $store 
            }
        }
    }
}

Function _GetWorkItemByRevision($WorkItem, $Revision, $store)
{
    if ($WorkItem -is [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem])
    {
        $ids = @($WorkItem.Id)
    }
    elseif ($WorkItem -is [int])
    {
        $ids = @($WorkItem)
    }
    elseif ($WorkItem -is [int[]])
    {
        $ids = $WorkItem
    }
    else
    {
        throw "Invalid work item ""$WorkItem"". Supply either a WorkItem object or one or more integer ID numbers"
    }

    if ($Revision -is [int] -and $Revision -gt 0)
    {
        foreach($id in $ids)
        {
            $store.GetWorkItem($id, $Revision)
        }
    }
    elseif ($Revision -is [int[]])
    {
        if ($ids.Count -ne $Revision.Count)
        {
            throw "When supplying a list of IDs and Revisions, both must have the same number of elements"
        }
        for($i = 0; $i -le $ids.Count-1; $i++)
        {
            $store.GetWorkItem($ids[$i], $Revision[$i])
        }
    }
    else
    {
        foreach($id in $ids)
        {
            $store.GetWorkItem($id)
        }
    }
}

Function _GetWorkItemByDate($WorkItem, $AsOf, $store)
{
    if ($WorkItem -is [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem])
    {
        $ids = @($WorkItem.Id)
    }
    elseif ($WorkItem -is [int])
    {
        $ids = @($WorkItem)
    }
    elseif ($WorkItem -is [int[]])
    {
        $ids = $WorkItem
    }
    else
    {
        throw "Invalid work item ""$WorkItem"". Supply either a WorkItem object or one or more integer ID numbers"
    }

    if ($AsOf -is [datetime[]])
    {
        if ($ids.Count -ne $AsOf.Count)
        {
            throw "When supplying a list of IDs and Changed Dates (AsOf), both must have the same number of elements"
        }
        for($i = 0; $i -le $ids.Count-1; $i++)
        {
            $store.GetWorkItem($ids[$i], $AsOf[$i])
        }
    }
    else
    {
        foreach($id in $ids)
        {
            $store.GetWorkItem($id, $AsOf)
        }
    }
}

Function _GetWorkItemByWiql($QueryText, $Macros, $Project, $store)
{
    if ($QueryText -notlike 'select*')
    {
        $q = Get-TfsWorkItemQuery -Query $QueryText -Project $Project

        if (-not $q)
        {
            throw "Work item query '$QueryText' is invalid or non-existent."
        }

        if ($q.Count -gt 1)
        {
            throw "Ambiguous query name '$QueryText'. $($q.Count) queries were found matching the specified name/pattern:`n`n - " + ($q -join "`n - ")
        }

        $QueryText = $q.QueryText
    }

    if (-not $Macros -and (($QueryText -match "@project") -or ($QueryText -match "@me")))
    {
        $Macros = @{}
    }

    if ($QueryText -match "@project")
    {
        if (-not $Project)
        {
            $Project = Get-TfsTeamProject -Current
        }

        if (-not $Macros.ContainsKey("Project"))
        {
            $Macros["Project"] = $Project.Name
        }
    }

    if ($QueryText -match "@me")
    {
        $user = $null
        $store.TeamProjectCollection.GetAuthenticatedIdentity([ref] $user)
        $Macros["Me"] = $user.DisplayName
    }

    _Log "Get-TfsWorkItem: Running query $QueryText"

    $wis = $store.Query($QueryText, $Macros)

    # foreach($wi in $wis)
    # {
    # if($Fields)
    # {
    # foreach($f in $Fields)
    # {
    # $wi | Add-Member -Name (_GetEncodedFieldName $f.ReferenceName) -MemberType ScriptProperty -Value `
    # {$f.Value}.GetNewClosure() `
    # {param($Value) $f.Value = $Value}.GetNewClosure()
    # }
    # }
    # }

    return $wis
}
Function Move-TfsWorkItem
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
    [OutputType('Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem')]
    Param
    (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [Alias("id")]
        [ValidateNotNull()]
        [object]
        $WorkItem,

        [Parameter(Mandatory=$true, Position=1)]
        [object]
        $Destination,

        [Parameter()]
        [object]
        $Area,

        [Parameter()]
        [object]
        $Iteration,

        [Parameter()]
        [object]
        $State,

        [Parameter()]
        [object]
        $History,

        [Parameter()]
        [object]
        $Collection
    )

    Begin
    {
        #_ImportRequiredAssembly -AssemblyName 'Microsoft.TeamFoundation.WorkItemTracking.Client'
        #_ImportRequiredAssembly -AssemblyName 'Microsoft.TeamFoundation.WorkItemTracking.WebApi'
    }

    Process
    {
        $wi = Get-TfsWorkItem -WorkItem $WorkItem -Collection $Collection

        $targetTp = Get-TfsTeamProject -Project $Destination -Collection $Collection
        $tpc = $targetTp.Store.TeamProjectCollection
        
        if ($Area)
        {
            $targetArea = Get-TfsArea $Area -Project $targetTp

            if (-not $targetArea)
            {
                if ($PSCmdlet.ShouldProcess("Team Project '$($targetTp.Name)'", "Create area path '$Area'"))
                {
                    $targetArea = New-TfsArea $Area -Project $targetTp -Passthru
                }
            }

            _Log "Moving to area $($targetTp.Name)$($targetArea.RelativePath)"
        }
        else
        {
            _Log 'Area not informed. Moving to root iteration.'
            $targetArea = Get-TfsArea '' -Project $targetTp
        }

        if ($Iteration)
        {
            $targetIteration = Get-TfsIteration $Iteration -Project $targetTp

            if (-not $targetIteration)
            {
                if ($PSCmdlet.ShouldProcess("Team Project '$($targetTp.Name)'", "Create iteration path '$Iteration'"))
                {
                    $targetIteration = New-TfsIteration $Iteration -Project $targetTp -Passthru
                }
            }

            _Log "Moving to iteration $($targetTp.Name)$($targetIteration.RelativePath)"
        }
        else
        {
            _Log 'Iteration not informed. Moving to root iteration.'
            $targetIteration = Get-TfsIteration '' -Project $targetTp
        }

        $targetArea = "$($targetTp.Name)$($targetArea.RelativePath)"
        $targetIteration = "$($targetTp.Name)$($targetIteration.RelativePath)"

        $patch = _GetJsonPatchDocument @(
            @{
                Operation = 'Add';
                Path = '/fields/System.TeamProject';
                Value = $targetTp.Name
            },
            @{
                Operation = 'Add';
                Path = "/fields/System.AreaPath";
                Value = $targetArea
            },
            @{
                Operation = 'Add';
                Path = "/fields/System.IterationPath";
                Value = $targetIteration
            }
        )

        if ($State)
        {
            $patch.Add( @{
                Operation = 'Add';
                Path = '/fields/System.State';
                Value = $State
            })
        }

        if ($History)
        {
            $patch.Add( @{
                Operation = 'Add';
                Path = '/fields/System.History';
                Value = $History
            })
        }

        if ($PSCmdlet.ShouldProcess("$($wi.WorkItemType) $($wi.Id) ('$($wi.Title)')", 
            "Move work item to team project '$($targetTp.Name)' under area path " +
            "'$($targetArea)' and iteration path '$($targetIteration)'"))
        {
            $client = _GetRestClient 'Microsoft.TeamFoundation.WorkItemTracking.WebApi.WorkItemTrackingHttpClient' -Collection $tpc
            $task = $client.UpdateWorkItemAsync($patch, $wi.Id)

            $result = $task.Result; if($task.IsFaulted) { throw 'Error moving work item' + ": $($task.Exception.InnerExceptions | ForEach-Object {$_.ToString()})" }

            return Get-TfsWorkItem $result.Id -Collection $tpc
        }
    }
}
<#
 
.SYNOPSIS
    Creates a new work item in a team project.
 
.PARAMETER Type
    Represents the name of the work item type to create.
 
.PARAMETER Title
    Specifies a Title field of new work item type that will be created.
 
.PARAMETER Fields
    Specifies the fields that are changed and the new values to give to them.
    FieldN The name of a field to update.
    ValueN The value to set on the fieldN.
    [field1=value1[;field2=value2;...]
 
.PARAMETER Project
    Specifies either the name of the Team Project or a previously initialized Microsoft.TeamFoundation.WorkItemTracking.Client.Project object to connect to. If omitted, it defaults to the connection opened by Connect-TfsTeamProject (if any).
 
For more details, see the Get-TfsTeamProject cmdlet.
 
.PARAMETER Collection
    Specifies either a URL/name of the Team Project Collection to connect to, or a previously initialized TfsTeamProjectCollection object.
 
When using a URL, it must be fully qualified. The format of this string is as follows:
 
http[s]://<ComputerName>:<Port>/[<TFS-vDir>/]<CollectionName>
 
Valid values for the Transport segment of the URI are HTTP and HTTPS. If you specify a connection URI with a Transport segment, but do not specify a port, the session is created with standards ports: 80 for HTTP and 443 for HTTPS.
 
To connect to a Team Project Collection by using its name, a TfsConfigurationServer object must be supplied either via -Server argument or via a previous call to the Connect-TfsConfigurationServer cmdlet.
 
For more details, see the Get-TfsTeamProjectCollection cmdlet.
 
.EXAMPLE
    New-TfsWorkItem -Type Task -Title "Task 1" -Project "MyTeamProject"
    This example creates a new Work Item on Team Project "MyTeamProject".
 
.INPUTS
    Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemType
    System.String
#>

Function New-TfsWorkItem
{
    [CmdletBinding(ConfirmImpact='Medium', SupportsShouldProcess=$true)]
    [OutputType('Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem')]
    Param
    (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)]
        [object] 
        $Type,

        [Parameter(Position=1)]
        [string]
        $Title,

        [Parameter()]
        [hashtable]
        $Fields,

        [Parameter()]
        [object]
        $Project,

        [Parameter()]
        [object]
        $Collection,

        [Parameter()]
        [switch]
        $SkipSave,

        [Parameter()]
        [switch]
        $Passthru
    )

    Process
    {
        if($PSCmdlet.ShouldProcess($Type, 'Create work item of specified type'))
        {
            $wit = Get-TfsWorkItemType -Type $Type -Project $Project -Collection $Collection

            $wi = $wit.NewWorkItem()

            if ($Title)
            {
                $wi.Title = $Title
            }

            foreach($field in $Fields)
            {
                $wi.Fields[$field.Key] = $field.Value
            }

            if (-not $SkipSave.IsPresent)
            {
                $wi.Save()
            }

            if ($Passthru)
            {
                return $wi
            }
        }
    }
}
<#
 
.SYNOPSIS
    Deletes a work item from a team project collection.
 
.PARAMETER Collection
    Specifies either a URL/name of the Team Project Collection to connect to, or a previously initialized TfsTeamProjectCollection object.
 
When using a URL, it must be fully qualified. The format of this string is as follows:
 
http[s]://<ComputerName>:<Port>/[<TFS-vDir>/]<CollectionName>
 
Valid values for the Transport segment of the URI are HTTP and HTTPS. If you specify a connection URI with a Transport segment, but do not specify a port, the session is created with standards ports: 80 for HTTP and 443 for HTTPS.
 
To connect to a Team Project Collection by using its name, a TfsConfigurationServer object must be supplied either via -Server argument or via a previous call to the Connect-TfsConfigurationServer cmdlet.
 
For more details, see the Get-TfsTeamProjectCollection cmdlet.
 
.INPUTS
    Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem
    System.Int32
#>

Function Remove-TfsWorkItem
{
    [CmdletBinding(ConfirmImpact="High", SupportsShouldProcess=$true)]
    Param
    (
        [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
        [Alias("id")]
        [ValidateNotNull()]
        [object]
        $WorkItem,

        [Parameter()]
        [object]
        $Collection
    )

    Process
    {
        $ids = @()

        foreach($wi in $WorkItem)
        {
            if ($WorkItem -is [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem])
            {
                $id = $WorkItem.Id
            }
            elseif ($WorkItem -is [int])
            {
                $id = $WorkItem
            }
            else
            {
                throw "Invalid work item ""$WorkItem"". Supply either a WorkItem object or one or more integer ID numbers"
            }

            if ($PSCmdlet.ShouldProcess("$($wi.WorkItemType) $id ('$($wi.Title)')", "Remove work item"))
            {
                $ids += $id
            }
        }

        if ($ids.Count -gt 0)
        {
            $tpc = Get-TfsTeamProjectCollection $Collection
            $store = $tpc.GetService([type] "Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore")

            $errors = $store.DestroyWorkItems([int[]] $ids)
        
            if ($errors -and ($errors.Count -gt 0))
            {
                $errors | Write-Error "Error $($_.Id): $($_.Exception.Message)"

                throw "Error destroying one or more work items"
            }
        }
    }
}
<#
.SYNOPSIS
Sets the contents of one or more work items.
 
.PARAMETER SkipSave
Leaves the work item in a "dirty" (unsaved) state, by not calling its Save() method. It is useful for when subsequents changes need to be made to the work item object before saving it. In that case, it is up to the user to later invoke the Save() method on the work item object to persist the changes.
 
.PARAMETER Project
Specifies either the name of the Team Project or a previously initialized Microsoft.TeamFoundation.WorkItemTracking.Client.Project object to connect to. If omitted, it defaults to the connection opened by Connect-TfsTeamProject (if any).
 
For more details, see the Get-TfsTeamProject cmdlet.
 
.PARAMETER Collection
Specifies either a URL/name of the Team Project Collection to connect to, or a previously initialized TfsTeamProjectCollection object.
 
When using a URL, it must be fully qualified. The format of this string is as follows:
 
http[s]://<ComputerName>:<Port>/[<TFS-vDir>/]<CollectionName>
 
Valid values for the Transport segment of the URI are HTTP and HTTPS. If you specify a connection URI with a Transport segment, but do not specify a port, the session is created with standards ports: 80 for HTTP and 443 for HTTPS.
 
To connect to a Team Project Collection by using its name, a TfsConfigurationServer object must be supplied either via -Server argument or via a previous call to the Connect-TfsConfigurationServer cmdlet.
 
For more details, see the Get-TfsTeamProjectCollection cmdlet.
#>

Function Set-TfsWorkItem
{
    [CmdletBinding(ConfirmImpact='Medium', SupportsShouldProcess=$true)]
    Param
    (
        [Parameter(ValueFromPipeline=$true, Position=0)]
        [Alias("id")]
        [ValidateNotNull()]
        [object]
        $WorkItem,

        [Parameter(Position=1)]
        [hashtable]
        $Fields,

        [Parameter()]
        [switch]
        $BypassRules,

        [Parameter()]
        [switch] 
        $SkipSave,

        [Parameter()]
        [object]
        $Collection
    )

    Process
    {
        if ($WorkItem -is [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem])
        {
            $tpc = $WorkItem.Store.TeamProjectCollection
            $id = $WorkItem.Id
        }
        else
        {
            $tpc = Get-TfsTeamProjectCollection -Collection $Collection
            $id = (Get-TfsWorkItem -WorkItem $WorkItem -Collection $Collection).Id
        }

        if ($BypassRules)
        {
            $store = New-Object 'Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore' -ArgumentList $tpc, [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStoreFlags]::BypassRules
        }
        else
        {
            $store = $tpc.GetService([type]'Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore')
        }

        $wi = $store.GetWorkItem($id)

        $Fields = _FixAreaIterationValues -Fields $Fields -ProjectName $wi.Project.Name

        if($PSCmdlet.ShouldProcess("Set work item fields $($Fields.Keys -join ', ') to $($Fields.Values -join ', '), respectively"))
        {
            foreach($fldName in $Fields.Keys)
            {
                $wi.Fields[$fldName].Value = $Fields[$fldName]
            }

            if(-not $SkipSave)
            {
                $wi.Save()
            }
        }
        
        return $wi
    }
}
Function _FixAreaIterationValues([hashtable] $Fields, $ProjectName)
{
    if ($Fields.ContainsKey('System.AreaPath') -and ($Fields['System.AreaPath'] -notmatch "'\\?$ProjectName\\.+'"))
    {
        $Fields['System.AreaPath'] = ("$ProjectName\$($Fields['System.AreaPath'])" -replace '\\', '\')
    }

    if ($Fields.ContainsKey('System.IterationPath') -and ($Fields['System.IterationPath'] -notmatch "'\\?$ProjectName\\.+'"))
    {
        $Fields['System.IterationPath'] = ("$ProjectName\$($Fields['System.IterationPath'])" -replace '\\', '\')
    }
    
    return $Fields
}

Function _GetEscapedFieldName([string] $fieldName)
{
    $fieldName = $fieldName.Trim()

    if(-not $fieldName.StartsWith('['))
    {
        $fieldName = '[' + $fieldName
    }

    if(-not $fieldName.EndsWith(']'))
    {
        $fieldName += ']'
    }

    return $fieldName
}

Function _GetEncodedFieldName([string] $fieldName)
{
    return $fieldName.Trim(' ', '[', ']') -replace '[/W]', '_'
}