Change Sitecore Item ID

The Sitecore Item ID is one of those properties that I have always been told that you can’t change. They were basically right because if you look at Sitecore.Data.Items.Item in the Sitecore.Kernel dll using dotPeek you will see that for the ID there is a get but no set property. It looks like this below

/// <summary>
/// Gets the ID of the item.
/// </summary>
/// <value>The ID.</value>
public ID ID
{
    get
    {
	return this._itemID;
    }
}

There were plenty of times where being able to change the Item ID would have helped such as

  • Someone deletes an item and it wasn’t sent to the Recycle Bin so you can’t easily restore it
  • A developer overwrites an item in TDS
  • A developer forgets to save the item in TDS
  • Someone forgets to inform the developer that they need to save a new item in TDS

When something like this happens it causes the items that reference the deleted item to be broken. You can create a new item and then use Powershell to update the references but now you have to run the Link Database and possibly even some of your indexes that reference the old item to make it work again. This might be a small task or a big one depending on how many items you have to deal with to fix the issue.

I wanted to find a better way to solve this problem, my code had to do the following

  • Serialize the item that needs to be updated
  • Find the Serialization item and update the contents to change the Item ID found to the new one
  • Deserialize the item (this creates 2 items with the same path but different IDs)
  • Move the children under the new item created from the original item
  • Delete the original item

If you just want to start using the code then I have a package you can download along with the steps to get going and then you will be set. If you are more adventurous, you can grab the code below and make the changes you need for your own Sitecore instance.

  • First install the package by clicking the download button
  • Open the Powershell console in Sitecore and then click the integrations button under the Settings Tab. This will allow for my code to show in the Content Menu)
  • If everything looks good then you should be able to right click on any Sitecore item in the Content Editor > Scripts > Serialize > Change Item ID
  • When you click it you should see a dialog that will look something like this below
  • You then enter a valid GUID to use.
  • Click Proceed
  • It will process your request. My code will check that it is valid and it will make sure that the GUID is not used any other items in Sitecore else it will stop any further processing. It sometimes takes a few seconds so don’t be too concerned
  • Once the process is done you might see the following error message, if you do just close that window and then click the parent item (/sitecore/content in my example), reopen the tree, click the item and the Item ID should now be changed. This is caused by my code having to delete the old item and since you had that selected in Content Editor it tries to find the item you selected but it no longer exists.
  • Congrats you are done

Below is the code that does the magic.

<#
    Change the Sitecore ID on a Sitecore Item

    This can be useful where a item has been deleted but the item was used everywhere 
    Maybe it was deleted by a Content Admin or a Developer forgot to add the item to TDS.
    Either way this will help with that issue so that the ID referenced in the indexes or databases will not be broken
#>

###########################################################################################################
# Global Variables

# Database to use for serialization
$global:db = "master"

# Get the Sitecore Database object
$global:dbToUse = [Sitecore.Configuration.Factory]::GetDatabase($global:db)

# Grab the item selected by the user and a few other things about the item
$itemSelected = Get-Item -Path .
$itemSelectedPath = $global:itemSelected.Paths.Path
$itemSelectedParentPath = $global:itemSelected.Parent.Paths.Path
$itemSelectedCurrentID = $global:itemSelected.ID.ToString()

# New ID to set the item to instead of its current ID
$global:newItemID = ""

# Used in the dialog to give the user information about the item they will be updating
$itemInfoMsg = "Item ID: $($itemSelectedCurrentID)<br>Item Path: $($itemSelectedPath)"

###########################################################################################################
# Functions

# This will verify that the GUID is in a valid format
function IsValidGUID($guid) {
   return $guid -match("^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$")
}

# This will check that the guid given is not already used by another item
function GetItemById($guid) {
   $item = $null

   try
   {
      $item = (Get-Item "$($global:db):" -ID $guid)
   }
   catch
   {
      # do nothing
   }
   
   return $item
}

# This will check that the ID given is valid and is not being used by any other item
function IsValidID($guid) {
    $isValid = $true

    if(![System.String]::IsNullOrEmpty($guid)) {

        # Make sure the guid is in the right format
        $isValid = (IsValidGUID -guid $guid)
        if(!$isValid) {
           Show-Alert -Title (GetAlertMsg -msg "GUID ($($guid)) was not a correctly formatted GUID to use, please use a different one");
           Exit
        }

        # Check that no other item is using the guid
        try
        {
            $item = (GetItemById -guid $guid)
            if($item -eq $null) {
               $isValid = $true
            } else {
               Show-Alert -Title (GetAlertMsg -msg "GUID ($($guid)) is being used by the item ($($item.Paths.Path)), please try a different one");
               Exit
            }
        }
        catch
        {
           $isValid = $true
        }
    } else {
      Show-Alert("You entered a blank GUID, please enter a valid GUID to continue");
      Exit
    }
    
    return $isValid
}

# This will get the .item file path if it exists
function GetSerializationPath($item) {
    [Sitecore.Data.Serialization.ItemReference] $itemReference = New-Object "Sitecore.Data.Serialization.ItemReference" -ArgumentList @(,$global:dbToUse.Name, $item.Paths.Path)
    
    if($itemReference -ne $null) {
       return [Sitecore.Data.Serialization.PathUtils]::GetFilePath($itemReference.ToString())
    } else {
       return [System.String]::Empty
    }
}

# This will check that the path for the serialized item exists
function ItemSerializationPathExists($serializedItemPath) {
   if(![System.String]::IsNullOrEmpty($serializedItemPath) -and (Test-Path $serializedItemPath)) {
      return $true
   }

   return $false
}

# Serialize the item and it will create a .item under the serialization folder /App_Data/serialization
function SerializeItem($item) {
   
   if($item -ne $null) {
       [Sitecore.Data.Serialization.Manager]::DumpItem($item)
   } else {
      Write-Host "Item is null...skipping serializing the item" -f yellow 
   }
}

# This will deserialize the item in the database
function DeserializeItem($item) {

    $serializedItemPath = GetSerializationPath -item $item
    
    # Check to make sure that the item has ever been serialized and then if so then load it up
    if((ItemSerializationPathExists -serializedItemPath $serializedItemPath)) {
        [Sitecore.Data.Serialization.LoadOptions] $options = New-Object "Sitecore.Data.Serialization.LoadOptions" -ArgumentList @(,$global:dbToUse)
        $options.ForceUpdate = $true
    
        New-UsingBlock (New-Object Sitecore.SecurityModel.SecurityDisabler) {
        New-UsingBlock (New-Object Sitecore.Data.DatabaseCacheDisabler) {
        New-UsingBlock (New-Object Sitecore.Data.Events.EventDisabler) {
            $newItemCreated = [Sitecore.Data.Serialization.Manager]::LoadItem($serializedItemPath, $options)               
        } } }
    } else {
      Write-Host "Serialized Item Path was not found...skipping" -f yellow
    }
}

# This will read the serialized item in the file path and update it will the new id
function ChangeSitecoreItemID($item) {

    $serializedItemPath = GetSerializationPath -item $item
 
    # Check to make sure that the item has ever been serialized and then if so then load it up
    if((ItemSerializationPathExists -serializedItemPath $serializedItemPath)) {
 
        $text = [System.IO.File]::ReadAllText($serializedItemPath);
 
        $text = ($text -replace $item.ID.ToString(), $global:newItemID)
 
        [System.IO.File]::WriteAllText($serializedItemPath, $text);
    } else {
      Write-Host "Serialized Item Path was not found...skipping" -f yellow
    }
}

# This will move all the direct children under the item to the new Item
function MoveChildrenToNewItem($item, $newItem) {

   $children = @()

   if($item.HasChildren) {

       # Make sure to use the item ID instead of the path to get the children because
       # both the item and newItem have the same path so it might grab the wrong one otherwise
       $children += Get-ChildItem "$($global:db):" -ID "$($item.ID.ToString())"

       $children | %{
           $child = $_

           Write-Host "Moving $($child.Name) to $($newItem.Paths.FullPath)" -f cyan

           # This is used insted of Move-Item because it only allows for me to do it by path and 
           # since I can have 2 items with the same path in this case that will not work correctly
           $child.MoveTo($newItem)
       }
   } else {
     Write-Host "No children were found for $($item.Paths.Path)" -f yellow
   }
}

# This will return the number of items with the same name in the same tree
function GetNumOfItemsByNameUnderSameParent($item) {
    return (Get-ChildItem -Path "$($global:db):$($item.Parent.Paths.Path)" | ?{ $_.Name -eq $item.Name } | %{ $_ }).Count
}

# Only delete the item if 2 exist so that it doesn't delete an existing item
# Make sure to move the children under the item first to the new item
function DeleteOldItem($item) {

    $numOfItemsWithSameName = (GetNumOfItemsByNameUnderSameParent -item $item)

    if($numOfItemsWithSameName -gt 1) {
   
       # This is used insted of Remove-Item because it only allows for me to do it by path and 
       # since I can have 2 items with the same path in this case that will not work correctly
       $item.Delete()
    }
}

# Used to make the Show-Alert messages a little nicer
function GetAlertMsg($msg) {
   $message = "<table>"
   $message += "<tr><td style='width: auto'>$($msg)</td></tr>"
   $message += "</table>"

   return $message
}

###########################################################################################################
# Main

$result = Read-Variable -Parameters `
    @{ Name = "Info"; Title="Selected Item Information"; Value="$itemInfoMsg"; Editor="info" },
    @{ Name = "newItemID"; Value=""; Title="Enter a New Item ID"; Tooltip=""; Placeholder=""} `
    -Description "This will replace the current Item ID to a new one" `
    -Title "Update Item ID to a new GUID" `
    -Icon "Applications/32x32/documents_gear.png" `
    -Width 500 `
    -Height 280 `
    -OkButtonName "Proceed" `
    -CancelButtonName "Abort" `

if($result -ne "ok") {
    Close-Window
    Exit
}

# Check that the GUID to use is valid or not
IsValidID -guid $global:newItemID

Write-Host "Changing $($itemSelectedPath)'s ID from ($($itemSelectedCurrentID)) to ($($global:newItemID))" -f cyan

# Create a serializable item (.item)
SerializeItem -item $itemSelected

# Update the current item with the new ID
ChangeSitecoreItemID -item $itemSelected

# Grab the .item with the new ID and update it in Sitecore
DeserializeItem -item $itemSelected

# Get the new item was that created after deserialization
$newItem = (GetItemById -guid $global:newItemID)

# If the new item was not created then show the user an alert and the results that were printed out
if($newItem -eq $null) {
   Show-Alert -Title (GetAlertMsg -msg "Item by the new ID was not created. Children were NOT moved and the Old Item was NOT deleted.  Please view the results");
   Show-Result
   Exit
}

# Move the children under the existing item to the new item created
MoveChildrenToNewItem -item $itemSelected -newItem $newItem

# Delete the existing item because a new item will be created in the 
# same tree with the same properties so the existing item needs to be deleted
DeleteOldItem -item $itemSelected

# Let the user know that they still need to publish the parent item and sub items so that it will be reflected in the web database
# Send a confirmation message to the user to make sure this is what they want to do
Show-Alert -Title (GetAlertMsg -msg "Make sure you do a publish of the parent item ($($itemSelectedParentPath)) and sub items so that you don't have 2 items with the same path ($($itemSelectedPath))") 

Write-Host "Done..." -f green

CJ Morgan

I am a software developer with over 15 years experience. In 2015, I joined BrainJocks and became a Sitecore certified developer. I have a BS degree in Computer Science from East Carolina University. I enjoy learning and solving issues that help me to become a better programmer. I also like helping others to learn because it mutually benefits me and the person learning–so win-win! In my spare time, I like playing video games, basketball, racquetball, and hanging out with my family.

More posts from CJ Morgan >

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back