Experience Sitecore ! | More than 200 articles about the best DXP by Martin Miles

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

Sitecore Cheat Sheets

While working with Sitecore, you have to keep in mind plenty of things, few of them not easy to remember. To simplify my life as a developer, I have created a compilation of various cheat sheets for various aspects of work with Sitecore. Having that printed on standard A4 papers I found that not quite convenient, and decided to re-create in more convenient format. Then I thought why not to issue that in a portable book format, and share with hundreds of people it may help.



Table of Content:

  • Sitecore Item API
  • ULR parameters
  • Sitecore queries
  • Sitecore PowerShell
  • Content Search API
  • Admin folder pages
  • Security
  • Databases
  • Mongo
  • Configuration
  • Core database
  • Rules engine
  • Helix / Habitat
  • Razor view extensions
  • Glass Mapper
  • Azure
  • Going live
  • ReSharper most used hotkeys
  • IIS and AS.NET
  • Icons in Sitecore
This is bare minimum I have already completed, however, if there are more of interesting topic - please let me know.

    Symposium 2017 and takeaways

    I've been anticipating that event for so long, and finally, I was there! So let's take a look at what's new have been presented.

    Sitecore 9 and xConnect brought a new era of Sitecore development. Starting from principally different installation approach, Sitecore brings multiple changes, the most significant of which are changes developers used to work with xDB (which now moved to SQL Server) and content search (which is now Solr by default). Now instead of calling xDB directly, we will be using xConnect API, which is very well documented, thanks to Martina Welander and her team.

    CRM connectors have been announced - for both Dynamics and Salesforce. 

    Zenith and Horizon - two Sitecore projects currently in development, but both are very promising. It will change the way we work with the platform, but as for now, there is no way too much information about.

    Marketing Automation has been re-worked to the best. Brimit, a Sitecore partner, has arranged a perfect demonstration stand of xConnect and marketing Automation working together on Sitecore 9, identifying contact's (visitor's) parameters from both online and offline channels and assigning them to certain profile pattern card, with sending them personalised email afterwards.

    Sitecore Cortex will be a new machine-learning technology to be used along with Sitecore xDB in order to increase personalisation and data analysis. 

    I spent decent time talking to guys from the team that builds the core of the platform and Express Upgrade Tool. The last one became a mature intelligent tool allowing do controlled upgrades from almost any recent version of Sitecore to version 9, with identifying all the potential issues and configuration breaking changes. I proud that have suggested few valuable ideas for the product about identifying any customises pipeline changes for the instance.

    Technical Preview of JavaScript Services.

    Thursday morning Symposium finished, but not for the 250+ lucky to be announced Sitecore MVPs of 2017. We have had 2 more thrilling days of MVP Summit.

    Almost everything we have been told or presented there has a "non-disclosure" label, so there's not too much to share. Summit was held at much faster pace, comparing to Symposium itself - longer up to 1 hour long deep presentations, with short 5-10 minutes breaks. We were given a great opportunity to challenge new Sitecore 9 training exam, so that whoever passes that test - becomes Sitecore 9 certified already (as I did!). We also took part in Round Table, where various Sitecore teams were presented at round tables, so MVPs were travelling from one interested table to another, raising questions, suggesting ideas and providing feedback.


    Sitecore 9 is out there


    So, finally, what we all have been waiting for, has happened - Sitecore 9 has been announced and released. To be honest, as an MVP, I already have had an access to a beta of the version 9 of Sitecore since late July. but after an official release, we now can publically talk about that.

    So, what has been new:

    Runtime for Sitecore 9 is .NET Framework 4.6.2 and most of Sitecore 9 NuGet libraries are built against that version of the framework.

    The installation process is the first you meet when trying to play with the platform. And it's worth saying that installation process has been totally reworked. So, welcome Sitecore Install Framework - Powershell modules for install and uninstall and a set of JSON config files that store steps for installing Sitecore. Each of these JSON files relies on a part of installation: xConnect, Solr and Sitecore itself. No SIM support (yet) available, unfortunately.

    I mentioned xConnect - that is a new component and probably the largest architectural change for the platform - a framework that allows you to read and write analytics data to xDB and search providers, keeping collections, processing, search (Solr) and reporting behind itself. Previously we have had 4 different APIs used to deal with xDB contact, depending on its state, so that entire method full of if /else clauses looked monstrous. Since now we do not do direct xDB calls and operate using XConnectClient. That allows updating contacts from any channel at any time and automatic indexing of data. Sitecore XP introduces client certificate authentication in order to communicate with xConnect server, that is a Windows service. Also, a good thing is that this framework is well documented.

    SQL Server 2016 becomes the only storage for all Sitecore databases - content data, xDB, marketing, processing, reporting etc. If you are desperate to keep Mongo - nothing to worry about it is still possible to do, or you may store in the cloud with Microsoft Azure SQL or Azure DocumentDB.

    Solr is now search provider by default, and it is also required for installation by Sitecore Installation Framework. Sitecore 9.0 supports version 6.6.2 and sorry not for having version 7.+ of Solr. It is commonly used to install Solr as a windows service, using nssm as the easiest.

    Sitecore Services Client (SSC) has got the ability to create oData REST service and API key management for securing access to APIs, that would be good for interacting with VUE or React JS.

    Those who struggled with Web Forms for Marketers module previously will enjoy new Sitecore Forms, featuring completely redesigned UX with drag and drop, easy styling, built analytics. Also, there is a multipage wizard for creating complex "split" forms. One can even create a form template and instantiate forms from it! Also, there is form and field metrics showing the corresponding performance.

    One more long waited feature is Dynamic Placeholders, now built into the platform, so that it may be called as @Html.Sitecore().DynamicPlaceholder("name"/*, some optional parameters*/). In general purpose, dynamic placeholders allow the use of the same placeholder key multiple times in the same rendering and across multiple renderings in the same placeholder. We have evidenced multiple implementations, so now good to have a universal standard.

    Marketing Automation tool is much better reworked. It provides a new way to create automated online campaigns in Sitecore. With a user-friendly drag-and-drop interface, Marketing Automation provides an extensible set of tools to processes contact enrollment in campaigns and activities. The Marketing Automation Engine is a stand-alone service that is responsible for enrolling contacts in plans and activities

    Express Migration Tool is already familiar to us. Since version 9 that it supports upgrades to 9 from almost any legacy version of Sitecore.

    Welcome, SPEAK 3, built on Angular 4, now using proper architecture. It is now reworked according to best industry standards, pulls data from Sitecore Services Client and unlike previous versions, it is item unaware. That means no more struggle with Rocks and lack of documentation. There is already a demo project you may download and rework.

    What previously caused pain for both developers, administrators, DevOps was way too much complicated configuration. Sitecore is a truly flexible platform, but the flexibility has hidden cost of every flexible component being registered via configuration, boosting compiled config up to hundreds of thousands of lines. That was taken into consideration and we were given configuration Roles and Layers. Now it becomes possible to configure whether a particular setting or config node should be patched for a specific server role using role:require attribute. An instance can be in one of the following roles: ContentDelivery, ContentManagement, Processing, Reporting, Standalone. Apart from roles, there is also a different level of configuration - the also layers: Sitecore, modules, custom - simple, flexible, allows less patching. With layers one can switch off an entire layer, it can be handy for Sitecore support providing a diagnostics, to disable anything apart from Sitecore to make ensure platform itself is working well.

    What is else new regarding Sitecore 9?
    JSS Framework is staging. Also coming soon Sitecore Commerce 9 with a proper inventory and support for federated authentication, SXA and Azure PaaS, CRM connectors for SalesForce and Dynamics for xConnect.

    Few SQL tricks helping to work with Sitecore

    1. Delete all the databases having a specific prefix, in the example below it will drop all the databases starting with habitat.dev.local_ - this may be helpful when Sitecore 9 installation broke half way down and you'd like to return everything to an initial point, also removing (partly) installed databases:

    USE MASTER
    GO
    
    DECLARE @dbnames NVARCHAR(MAX)
    DECLARE @statement NVARCHAR(MAX)
    SET @dbnames = ''
    SET @statement = ''
    SELECT @dbnames = @dbnames + ',[' + name + ']' FROM sys.databases WHERE NAME LIKE 'habitat.dev.local_%'
    IF LEN(@dbnames) = 0
        BEGIN
        PRINT 'no databases to drop'
        END
    ELSE
        BEGIN
        SET @statement = 'drop database ' + SUBSTRING(@dbnames, 2, LEN(@dbnames))
        PRINT @statement
        EXEC sp_executesql @statement
        END
    


    2. Backup all SQL database having some prefix (in this case, habitat.dev.local_) to a folder (like C:\DatabaseBackups\):

    DECLARE @name VARCHAR(50) -- database name  
    DECLARE @path VARCHAR(256) -- path for backup files  
    DECLARE @fileName VARCHAR(256) -- filename for backup  
    DECLARE @fileDate VARCHAR(20) -- used for file name
    DECLARE @scPrefix VARCHAR (20) -- Sitecore database prefix
    
    -- specify database backup directory
    SET @path = 'C:\DatabaseBackups\' 
    
    -- specify filename format
    SELECT @fileDate = CONVERT(VARCHAR(20),GETDATE(),112) 
    
    -- specify the Sitecore db prefix
    SET @scPrefix = 'habitat.dev.local_'
    DECLARE db_cursor CURSOR FOR SELECT name FROM master.dbo.sysdatabases WHERE CharIndex(@scPrefix, name) = 1 OPEN db_cursor FETCH NEXT FROM db_cursor INTO @name WHILE @@FETCH_STATUS = 0 BEGIN SET @fileName = @path + @name + '_' + @fileDate + '.BAK' BACKUP DATABASE @name TO DISK = @fileName FETCH NEXT FROM db_cursor INTO @name END CLOSE db_cursor DEALLOCATE db_cursor

    3. Shrink all databases larger than a certain number of megabytes (stored in @megabytesMoreToShrink variable):

    DECLARE @megabytesMoreToShrink INT SET @megabytesMoreToShrink = 100
    
    DECLARE @name NVARCHAR(MAX) 
    DECLARE @logical_name NVARCHAR(MAX) 
    
    DECLARE db_cursor CURSOR FOR 
    SELECT db.name AS DBName, mf.Name AS Logical_Name
    FROM sys.master_files mf INNER JOIN sys.databases db ON db.database_id = mf.database_id Where type_desc = 'LOG' AND (size*8)/1024 > @megabytesMoreToShrink
    
    OPEN db_cursor  
    FETCH NEXT FROM db_cursor INTO @name, @logical_name  
    
    WHILE @@FETCH_STATUS = 0  
    BEGIN  
    
                  exec('USE ' + @name + ' ;ALTER DATABASE ' + @name + ' SET RECOVERY SIMPLE;DBCC SHRINKFILE ('''+@logical_name +''', 1);ALTER DATABASE '+@name +' SET RECOVERY FULL')
    
                  FETCH NEXT FROM db_cursor INTO @name, @logical_name  
    END  
    
    CLOSE db_cursor  
    DEALLOCATE db_cursor

    4. Change sa password for SQL instance, you do not need to know the previous password at all, but need to have an administrative access, for sure. Run osql tool, it's path should be added to the environment variable, but if it's not there, please find it here: "%ProgramFiles%\Microsoft SQL Server\130\Tools\Binn\OSQL.exe":

    osql -S SITECOREDEV -E
    1> sp_password NULL, 'SA_PASSWORD','sa'
    2> GO

    5. Some basic item's operations, such as retrieving by ID, by parent and retrieving fields and versions:

    -- Get item by Sitecore ID
    SELECT TOP 1000 *
      FROM [habitat_Master].[dbo].[Items]
      WHERE ID = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}'
    
    -- Get item by ID of parent in Sitecore
    SELECT TOP 1000 *
      FROM [habitat_Master].[dbo].[Items]
      WHERE ID = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}'
    
    -- Enumerate all the versioned fieds for an item in Sitecore by item ID
    SELECT TOP 1000 
      i.Name, v.[Id], [ItemId], [LANGUAGE], [FieldId], [Value], v.[Created], v.[Updated]
      FROM [habitat_Master].[dbo].[versionedFields] v
      JOIN [habitat_Master].[dbo].Items i 
        ON v.FieldId = i.ID
      WHERE ItemId = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}'
      ORDER BY [Name]

    5. Some basic item's operations, such as retrieving by ID, by parent and retrieving fields and versions:

    -- Get item by Sitecore ID
    SELECT TOP 1000 *
      FROM [habitat_Master].[dbo].[Items]
      WHERE ID = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}'
    
    -- Get item by ID of parent in Sitecore
    SELECT TOP 1000 *
      FROM [habitat_Master].[dbo].[Items]
      WHERE ID = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}'
    
    -- Enumerate all the versioned fieds for an item in Sitecore by item ID
    SELECT TOP 1000 
      i.Name, v.[Id], [ItemId], [LANGUAGE], [FieldId], [Value], v.[Created], v.[Updated]
      FROM [habitat_Master].[dbo].[versionedFields] v
      JOIN [habitat_Master].[dbo].Items i 
        ON v.FieldId = i.ID
      WHERE ItemId = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}'
      ORDER BY [Name]

    6. One of the most popular snippets, to restore default admin / b password. Please keep in mind running that against core database:

    -- use core database
    UPDATE dbo.aspnet_Membership
    SET
        Password='qOvF8m8F2IcWMvfOBjJYHmfLABc=',
        PasswordSalt='OM5gu45RQuJ76itRvkSPFw=='
    WHERE
        UserId = (SELECT UserId FROM dbo.aspnet_Users WHERE UserName = 'sitecore\Admin')

    7. Copy users and all their data including passwords from core database of one instance to another:

    -- Copy all non-existing users and their data from OLD core database to NEW core database
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_Applications SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_Applications 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_Applications WHERE [Sitecore.NEW.Core].dbo.aspnet_Applications .ApplicationId = [Sitecore.OLD.Core].dbo.aspnet_Applications.ApplicationId)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_Membership SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_Membership 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_Membership WHERE [Sitecore.NEW.Core].dbo.aspnet_Membership .UserId = [Sitecore.OLD.Core].dbo.aspnet_Membership.UserId)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_Paths SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_Paths 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_Paths WHERE [Sitecore.NEW.Core].dbo.aspnet_Paths .PathId = [Sitecore.OLD.Core].dbo.aspnet_Paths.PathId)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_PersonalizationAllUsers SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_PersonalizationAllUsers 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_PersonalizationAllUsers WHERE [Sitecore.NEW.Core].dbo.aspnet_PersonalizationAllUsers.PathId = [Sitecore.OLD.Core].dbo.aspnet_PersonalizationAllUsers .PathId)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_PersonalizationPerUser SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_PersonalizationPerUser 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_PersonalizationPerUser WHERE [Sitecore.NEW.Core].dbo.aspnet_PersonalizationPerUser.PathId = [Sitecore.OLD.Core].dbo.aspnet_PersonalizationPerUser .PathId)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_Profile SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_Profile 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_Profile WHERE [Sitecore.NEW.Core].dbo.aspnet_Profile.UserId = [Sitecore.OLD.Core].dbo.aspnet_Profile.UserId)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_Roles SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_Roles 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_Roles WHERE [Sitecore.NEW.Core].dbo.aspnet_Roles.RoleName = [Sitecore.OLD.Core].dbo.aspnet_Roles.RoleName)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_SchemaVersions SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_SchemaVersions 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_SchemaVersions WHERE [Sitecore.NEW.Core].dbo.aspnet_SchemaVersions.Feature = [Sitecore.OLD.Core].dbo.aspnet_SchemaVersions.Feature)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_Users SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_Users 
    WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_Users WHERE [Sitecore.NEW.Core].dbo.aspnet_Users.UserName = [Sitecore.OLD.Core].dbo.aspnet_Users.UserName)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_UsersInRoles SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_UsersInRoles 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_UsersInRoles WHERE [Sitecore.NEW.Core].dbo.aspnet_UsersInRoles.UserId = [Sitecore.OLD.Core].dbo.aspnet_UsersInRoles.UserId)
    
    INSERT INTO [Sitecore.NEW.Core].dbo.aspnet_WebEvent_Events SELECT * FROM [Sitecore.OLD.Core].dbo.aspnet_WebEvent_Events 
        WHERE NOT EXISTS (SELECT * FROM [Sitecore.NEW.Core].dbo.aspnet_WebEvent_Events WHERE [Sitecore.NEW.Core].dbo.aspnet_WebEvent_Events.EventId = [Sitecore.OLD.Core].dbo.aspnet_WebEvent_Events .EventId)

    8. Delete children item of an item with a given ID directly from SQL (beware item cache). Please make sure you execute that in a context of a Sitecore database (ie. call use Sitecore_master before running this):

    DECLARE @parentId AS UNIQUEIDENTIFIER; SET @parentId = '{29267838-AAB2-415F-B07A-D006719CD088}'
    DECLARE @RowsToProcess  INT
    DECLARE @CurrentRow     INT
    DECLARE @SelectCol1     UNIQUEIDENTIFIER
    DECLARE @Items2Delete TABLE (RowID INT NOT NULL PRIMARY KEY IDENTITY(1,1),ID UNIQUEIDENTIFIER)
    
    SET @parentId = '{29267838-AAB2-415F-B07A-D006719CD088}'
    
    INSERT INTO @Items2Delete (ID)
    SELECT [Descendant] AS ID
      FROM [dbo].[Descendants]
      WHERE Ancestor = @parentId
    SET @RowsToProcess=@@ROWCOUNT
    PRINT  @RowsToProcess
    
    INSERT INTO @Items2Delete (ID)
    SELECT ID FROM [dbo].[Items] WHERE ParentID = @parentId
    SET @RowsToProcess=@@ROWCOUNT
    PRINT  @RowsToProcess
    
    SET @CurrentRow=0
    WHILE @CurrentRow<@RowsToProcess
    BEGIN
        SET @CurrentRow=@CurrentRow+1
        SELECT
            @SelectCol1=ID
            FROM @Items2Delete
            WHERE RowID=@CurrentRow
    
      DECLARE @id AS UNIQUEIDENTIFIER;
    
      SET @id = @SelectCol1
      PRINT @id
    
      EXEC sp_executesql N'DELETE FROM [Items]
            WHERE [ID] = @itemId
    
            DELETE FROM [SharedFields]
            WHERE [ItemId] = @itemId
    
            DELETE FROM [UnversionedFields]
            WHERE [ItemId] = @itemId
    
            DELETE FROM [VersionedFields]
            WHERE [ItemId] = @itemId',N'@itemId UNIQUEIDENTIFIER',@itemId=@id
    
      EXEC sp_executesql N'DELETE FROM [Descendants] WHERE [Descendant] = @itemId',N'@itemId uniqueidentifier',@itemId=@id
      EXEC sp_executesql N' DELETE FROM [Links] WHERE [SourceItemID] = @itemID AND [SourceDatabase] = @database',N'@itemID uniqueidentifier,@database nvarchar(6)',@itemID=@id,@database=N'master'
      EXEC sp_executesql N'DELETE FROM [Tasks] WHERE [ItemID] = @itemID AND [Database] = @database',N'@itemID uniqueidentifier,@database nvarchar(6)',@itemID=@id,@database=N'master'
    END

    Hope that helps you!

    Updating existing presentation details of base template's standard values after it has been set on derived templates

    Problem: let's imagine the situation when you may have some complex template inheritance. You would like to set presentation details for each of these templates' standard values, somehow like below:

    A - define layout, and header/footer common holders that will be shared for all sites (we've got a multisite setup for now)

    B - define footer and header on a site level so that they remain the same within each of implemented sites 

    C - define base page template that will add the rest of presentation shared between the pages (but not homepage)

    Once you set presentation for standard values of A, you can go to standard values of B and see these changes, so that you add only B-specific components; the same exact story will be when you will go next to std. values of C an so on. So far, so good.

    The problem occurs when you want to modify presentation coming with a base template after a derived presentation is already set. In that case, you will not see any difference, that may be seen a weird for a while. In our example, imagine you've set presentation for std. values of all three templates - A, B and then C, and then decided to add one more component to a presentation on A template (or change datasource item of existing etc.). You do changes for std. values of A, save it and as you see - these changes come into a play for template A, however, once you open B or C  they won't be there...

    Explanation: let's think what in fact Standard Values are - just the default values for each of field defined in that (or parent) template. In the second case if a field has standard values for both parent and inherited template - it simply overrides parent value with inherited child's value. But, wait for a second - presentation is also stored in fields of Standard Template that all pages inherit from, how that makes possible, does it simply override?

    No, for such particular cases when presentation fields are involved - override behaviour would not work at all. Let's look at our example - template A defines layout and header/footer sublayout and all that goes within __Renderings field - it's where (shared, not versioned) presentation is stored in XML-serialised format. But then, it would be overridden by setting concrete footer with no layout. Since it is the same field - it will lose layout at template B level and entire behaviour does not make any sense. To address this issue Sitecore implements a feature called Layout Deltas - so that presentation fields are not stupidly overwritten. Instead, after we defined default presentation for template A, it goes as is, as A does not have any base template with presentation set. But when setting presentation for B - it will only save the difference between itself and presentation of base template (if exists). When page is being rendered, Sitecore is wise enough to construct resulting page presentation from all the base templates only adding deltas with each derived template. That is how Layout Deltas work.

    One may create multilevel presentation inheritance of standard values, appending more and more presentation on each of derived levels. However, when we want to adjust the presentation of base template (A) of current template (B) - changes will be affected only for A, but not B or C if they already have layout deltas defined. That behaviour raises questions without a doubt.

    Solution: what we need to do to in order to ensure changing presentation details for A will be affected for all derived templates' items is to recourse inheritance tree of A and re-calculate layout delta for each of them with recent updates from A. In order to get this done I have re-worked a solution suggested by ... The difference is that since that time we now got a feature called Versioned Layouts, so that we need to operate both fields - __Renderings and __Final Renderings correspondingly. Apart from that I have tested it for a while and fixed few of stability issues.

    Implementation: when we change presentation - we change the field, so eventually the holding item is being updated. In order to intercept this we add a pipeline processor for item:saving event:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
      <sitecore>
        <events>
          <event name="item:saving">
            <handler type="Sitecore.Foundation.Presentation.Services.LayoutInheritance, Sitecore.Foundation.Presentation" method="OnItemSaving"/>
          </event>
        </events>
      </sitecore>
    </configuration>
    
    

    And the code. I am using XmlDeltas.GetDelta() and XmlDeltas.ApplyDelta() static classes of Sitecore.Data.Fields namespace to make this work.

    using System;
    using Sitecore;
    using Sitecore.Data;
    using Sitecore.Data.Events;
    using Sitecore.Data.Fields;
    using Sitecore.Data.Items;
    using Sitecore.Data.Managers;
    using Sitecore.Data.Templates;
    using Sitecore.Diagnostics;
    using Sitecore.Events;
    using Sitecore.Globalization;
    using Sitecore.SecurityModel;
    
    namespace Sitecore.Foundation.Presentation.Services
    {
        public class LayoutInheritance
        {
            public void OnItemSaving(object sender, EventArgs args)
            {
                Item item = Event.ExtractParameter(args, 0) as Item;
                PropagateLayoutChanges(item);
            }
    
            private void PropagateLayoutChanges(Item item)
            {
                if (StandardValuesManager.IsStandardValuesHolder(item))
                {
                    Item oldItem = item.Database.GetItem(item.ID, item.Language, item.Version);
    
                    PropagateLayoutChangesForField(item, oldItem, FieldIDs.LayoutField);
                    PropagateLayoutChangesForField(item, oldItem, FieldIDs.FinalLayoutField);
                }
            }
    
            private void PropagateLayoutChangesForField(Item item, Item oldItem, ID layoutField)
            {
                string layout = item[layoutField];
                string oldLayout = oldItem[layoutField];
    
                if (layout != oldLayout)
                {
                    string delta = XmlDeltas.GetDelta(layout, oldLayout);
                    foreach (Template templ in TemplateManager.GetTemplate(item).GetDescendants())
                    {
                        ApplyDeltaToStandardValues(templ, layoutField, delta, item.Language, item.Version, item.Database);
                    }
                }
            }
    
            private void ApplyDeltaToStandardValues(Template template, ID layoutField, string delta, Language language, Sitecore.Data.Version version, Database database)
            {
                if (template?.StandardValueHolderId != (ID)null)
                {
                    try
                    {
                        Item item = ItemManager.GetItem(template.StandardValueHolderId, language, version, database, SecurityCheck.Disable);
    
                        if (item == null)
                        {
                            Log.Warn($"Foundation.Presentation: Item is null {template.StandardValueHolderId} in database {database.Name}", template);
                            return;
                        }
    
                        Field field = item.Fields[layoutField];
    
                        if (field == null)
                        {
                            Log.Warn($"Foundation.Presentation: Field is null in item {item.ID} in database database.Name", item);
                            return;
                        }
    
                        if (!field.ContainsStandardValue)
                        {
                            string newFieldValue = XmlDeltas.ApplyDelta(field.Value, delta);
                            if (newFieldValue != field.Value)
                            {
                                using (new EditContext(item))
                                {
                                    LayoutField.SetFieldValue(field, newFieldValue);
                                }
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        Log.Info($"Foundation.Presentation: Exception {e.Message}", e);
                        throw;
                    }
                }
            }
        }
    }
    
    As I am using Helix in my development, I created a foundation module Presentation and placed the above code and config into it correspondingly. 

    Hope this helps someone!

    SUGCon 2017 insights and takeaways

    I have attended SUGCon in Amsterdam this year, as usual - it was a blast! What I like Sitecore events for - they always have such an enthusiastic atmosphere of hundreds of best technology professionals united together "on the same boat".

    This time I was proud to announce the winners of the community award - The Most Productive Sitecore Authors of 2016. Three luxury looking trophies found their owners, well-deserved guys!


    Anyway, I have got few thoughts/takeaways from that wonderful event.

    Sitecore Helix has become a proven and reliable development approach for Sitecore already. The more complex your solution is - the more likely you'd use Helix. I am now working on a multinational and multi-project implementation for an insurance service provider and we are using Helix. Given that we have also a distributed development teams, it became crucial that we are doping the development under the same guidelines and each new team member, already familiar with Helix and SOLID principles, becomes productive quite quickly. Also deployment, quality assurance as well as the rest of day-to-day activities - are going to the same standards. My own opinion on Helix is that "if it is cooked well - it serves you well"

    My greatest impression after attending SUGCon was after I saw how Mark Stiles combined Microsoft Cognitive Services with Sitecore. The future is already here!

    SXA and Sitecore Commerce are also two going trends. As you know, Sitecore has purchased Commerce Server and has now had intensively committed to the product. Sitecore Experience Accelerator is a different beast, that would best suit for large brands with multisite implementations, that simplifies governance and maintainability of them.

    Sitecore PaaS becomes closer and closer to the real world. I'd personally not use it now, due to many (not yet sorted) incompatibilities, but the pace is impressing. Christof Claessens has presented about the way Sitecore works on Azure PaaS and relevant modules compatibilities.

    Stephen Pope presented Sitecore Publishing Service 2.0 - an app written with .NET core that now increases publishing performance dramatically. The idea behind service is that it separates publishing process from the Sitecore UI by creating standalone service with a publishing queue, that allows aligning publishing load.

    Nick Hills from True Clarity gave a great portion of insights on how Sitecore DevOps on AWS and personalisation are done for such a large implementation as for EasyJet.

    JavaScript Service (or simply JSS) - another great impression was the last event at SUGCon, presented by Alex Shyba and Adam Weber. Smoothless javascript and front-end stuff working with node.js and Sitecore layouts will bring an excellent user experience. However, that was more a demo of the proof of concept and team needs to complete the development.

    There was much more, but this is what comes to my mind (and from the notes) for the moment...

    And finally, those 250+ of lucky to become Sitecore MVPs of 2017 were awarded a symbol of commitment to the Sitecore and its community:

    Helix + Glass Mapper + T4 Templates = Code Generation

    The objective of given exercise is to achieve automatic code generation of Glass Mapper model (an interface) from a corresponding serialized interface template.

    First of all, you need to install Visual Studio Extension for T4 templates: for Visual Studio 2017 or 2015. Also very recommend installing Devart T4 Editor - a markup highlighter and intellisense for T4 templates, that is also a plugin for Visual Studio 2017 or 2015.

    Another prerequisite is Glass Mapper itself, obviously. I would advise going with BoC.Glass.Mapper.Sc by Chris Van Steeg from Sitecore NuGet repository. The fork contains several fixes for code-first and content search usage (project fork GitHub page).

    The main thing you need is Sitecore CodeGenerator, located in https://github.com/hermanussen/sitecore.codegenerator

    You'll also need to have "base" templates - GlassGenerator.tt and GlassMappedClassTemplate.tt as below:


    SitecoreTemplates.tt - is the template you are going to copy to each module where you may need to generate models, you may want to rename each instance of it to reflect module name.

    Once copied, just paste a list of your interfaces templates into SitecoreTemplates.tt (or WhateverYouCallIt.tt, in the example below I called that same as a module name) and save the file, or optionally right-clicking "SitecoreTemplates.tt" and choosing "Run Custom Tool". You'll see you interfaces magically generated as the child items of *.tt file, with all the references set up (in my case ILink.gen.cs and ILinkMenuItem.gen.cs)

    NavigationTemplates.tt (from Feature/Navigation module):

    <#@ template language="C#" debug="True" #>
    <#@ output extension="gen.txt" #>
    <#@ include file="T4Toolbox.tt" #>
    <#@ include file="$(SolutionDir)tools\CodeGeneration\T4.Templates\base\GlassGenerator.tt" #>
    <#
    	GlassGenerator generator = new GlassGenerator(
    			"master",
    			new [] { 
    					"/sitecore/templates/Feature/Navigation/_Link",
    					},
    			(fieldId, fieldOptions) =>
    				{
    				});
        generator.Run();
        
    	WriteLine("These files were generated:");
    #>
    



    I am attaching these TT files for your convenience below:

    GlassGenerator.tt (3.6KB), GlassMappedClassTemplate.tt (3.8KB) and SitecoreTemplates.tt (1020B).

    If you need to change the way how the code is being generated please consider modifying "GlassGenerator.tt" and "GlassMappedClassTemplate.tt" file located in "Configuration\CodeGeneration\Templates\Base\" solution folder.


    Notes: you may have Sitecore.CodeGenerator as a part of your solution, or unload from it - code generation still would work, just please make sure template files are referencing the right paths. Samples provided above are configured as at the image above.


    Helix: An approach for restoring WebRoot to an initial state of vanilla Sitecore installation

    I have an interesting approach to organizing work with Helix. Imagine a quite standard situation, when you have an already running solution with a code base outside web folder, into which gulp script deploys into. Actually, default Helix scenario.

    So, you've been working on some module, then halfway down you've renamed that module or simply decided to remove it from solution. After changes finishing with renaming /deleting, and following redeploying the solution, you may potentially get an error (or not get it), but in any case, sooner or later you find out that your web folder still has an old module in a previous stage, dll and configs. To fix that, a DLL of that module needs to be deleted from bin folder manually, as well as all the corresponding old config files from App_Config/Include folder. Not a nice approach...

    Things get even worse if you continuously repeat that delete/rename exercise with other modules, increasing chances of malfunction or false positive, and the far you go with that - the more complicated is to clean up the ends. Can you imagine how many unwanted trails are left in your web folder?

    For sanity purpose, it is the best practice to ensure that your code is deployed into a clean copy of Sitecore at webroot so that you can be sure that it functions as expected (or not). So how would you achieve obtaining an exactly the same clean Sitecore as you have installed initially, and make this procedure super quick and repeatable? In case you are working on Sitecore previous to version 9 and have used SIM tool in order to install your Sitecore instance, then the answer is obvious - just use SIM backup / restore for that Sitecore instance, quick and reliable. But what if not? What if you are using the latest Sitecore 9?

    The method I am suggesting may be sort of arguably, however, it does the job perfectly, quickly and keeps things consistent and at the same place. What I do - I create an isolated git branch called SitecoreFiles_CM at the same git repository where the main codebase sits, and commit an initial state of Sitecore to that branch. Saying initial I mean exactly the same files as they were installed, before Sitecore was even ever accessed - a clean installation. Of course, not all the files are pushed to remote: things like configurations with my individual (for my local dev instance) passwords do not need to be shared with other developers as well as Sitecore license file. That's why on top of that branch that is pushed to remote, I create one more local commit having these individual settings/changes that I do not push. When I have a need to restore vanilla Sitecore, I navigate to web folder and restore folder to that commit. The next step is git clean command required to get rid of the rest of unversioned files on top of vanilla Sitecore that were deployed from the codebase. As simple, as functional.

      git reset --hard
      git clean -fdx
    


    Both codebase and the corresponding vanilla Sitecore are kept together at the same git repo, for consistency.

    Additional benefit: with git diff, you may always see the difference between current web folder state and initial clean install. Not to say, that you can restore each individual file from that set!

    When doing platform version upgrades, I upgrade my major codebase along with SitecoreFiles_CM branch, so they again remain consistent Looking ahead, no one stops you from having also a SitecoreFiles_CD branch with a CD set of files, however that is outside of current discussion.

    Hope this helps!

    Creating "Change item ID" module. Part 1 - Identifying the requirements

    I am working on a bit of unusual solution at he moment and one of the "features" I was surprised to own was and external feed of data that serves me the content in various formats including GUIDs of items existing in my Sitecore database.Since feed is not well flexible, I decided to become flexible myself and one of challenges I rose for myself was changing IDs of existing items in Sitecore.

    As you know, ID is read-only property of Item in Sitecore, it is being generated upon item's creation and remains the same for entire lifetime. Driven by curiosity, I started investigating how to achieve that and decided to look up any existing implantation. What can be similar to changing ID? Of course, that would be Rename feature.

    Let's take a look at Rename icon in Content Editor context menu, when right clicking on an item. It is located at /sitecore/content/Applications/Content Editor/Context Menues/Default/Rename and sends the following message

    item:rename(id=$Target)

    that calls rename command. So why not to create one more command right after Rename that will send message:

    item:changeid(id=$Target)

    Of course, we need to associate that to a corresponding business class that inherits from Command:

    <commands>
      <command name="item:changeid" type="ChangeId.ChangeIdCommand,ChangeId" patch:after="command[@name='item:rename']" />
    </commands>

    Let's follow Sitecore architectural principles and implement our change ID logic in a form of a pipeline, let's call it uiChangeIdForItem. In that case new ChangeIdCommand wil do nothing else but trigger the pipeline:

    [Serializable]
    public class ChangeIdCommand : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (context.Items.Length != 1 || context.Items[0] == null)
            {
                return;
            }
    
            StartPipeline("uiChangeIdForItem", context.Items[0]);
        }
    
        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (context.Items.Length != 1)
            {
                return CommandState.Disabled;
            }
    
            Item obj = context.Items[0];
            if (obj.Appearance.ReadOnly || !obj.Access.CanRename() || IsLockedByOther(obj))
            {
                return CommandState.Disabled;
            }
    
            return base.QueryState(context);
        }
    
        private static void StartPipeline(string pipelineName, Item item)
        {
            Assert.ArgumentNotNullOrEmpty(pipelineName, "pipelineName");
            Assert.ArgumentNotNull(item, "item");
    
            NameValueCollection parameters = new NameValueCollection();
            parameters.Add("database", item.Database.Name);
            parameters.Add("id", item.ID.ToString());
            parameters.Add("language", item.Language.ToString());
            parameters.Add("version", item.Version.ToString());
    
            Context.ClientPage.Start(pipelineName, parameters);
        }
    }
    

    Okay, but what would processors our pipeline consist from? To answer that we need to understand how it will be renaming an item and what additional -pre and -post processing steps may occur, passing parameters between steps and fallback scenarios. So let's take into consideration the following concerns:

    1. Changing ID cannot be done directly in the code by using Sitecore API, that isn't something that is or will be supported in future. But by using API we can serialize and deserialize an item, so changing ID would be a matter of simply changing several bytes in plain text file. So based on that we already have a consequence of three processors: Serialize ==> ChangeId ==> Deserialize.

    2. Deserialization of an item with modified ID will create a new item with new ID that would be an absolute clone of original item rather then updating ID of existing item. That means I can end up having two exact items, so I need to delete the one having original ID. That adds RemoveOriginalItem processor somewhere at the end of pipeline, when the rest is done.

    3. To obtain new item I would need it to get from somewhere, so let's ask a user to provide that by raising a popup input box from UI. That adds an additional processor, let's call it GetNewName.

    4. Changing ID is a dangerous task so a user should clearly realise what he/she is doing and understand consequences, that should be ideally an admin or at least a developer. So we should not allow to perform this operation to everyone but should check whether a user has appropriate permissions. Of course, permissions can be set externally from Security Editor but that's only UI and can be bypassed by sending the same command message from JavaScript or developers tools in browser. That's why it is essential to verify permissions at the backend prior to doing anything else, so that results in CheckPermissions processor in the beginning of pipeline.

    5. Items in SItecore are organised hierarchically so if we change ID of an item by deserializing it into a new clone item, children still remain under original item that we are right about to delete that will remove children as well. So to correct that we need to explicitly move all the descendants under a new item. That can become an addtional MoveChildren processor after deserialization process.

    6. Also if original item was referenced by any other items, those references will become broken once delete original item will be deleted so need to implement UpdateReferences processor. If there are many references to an item it may take a considerable time to update them all, so in user-friendly manner we need to prompt that and ask whether a user still want to process. Why not a candidate for yet another CheckNumberOfReferences processor?

    Combining 10 above processors together in the right order makes will look as below:

    <processors>
      <uiChangeIdForItem>
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CheckPermissions" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="VerifyUnsupportedTypes" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="GetNewName" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CheckLinks" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="Serialize" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="ChangeId" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="Deserialize" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CopyTemplateChildren" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="RepairLinks" />
        <processor mode="on" type="ChangeId.ForItem,ChangeId" method="RemoveSourceItem" />
      </uiChangeIdForItem>
    </processors>

    So far, so good! Once we mentioned all the requirements and are certain about the steps, let's do the actual implementation in second part of this blog post.

    Creating "Change item ID" module. Part 2 - The implementation

    In the first part of his blog post we have identified the requirements, designed the consequence of uiChangeIdForItem pipeline processors, as well as created necessary Sitecore plumbing for that. So now let's focus on actual business logic and processors implementation.

    The implementation of processors:

    1. CheckPermissions
    2. VerifyUnsupportedTypes
    3. GetNewName
    4. CheckNumberOfReferences
    5. Serialize
    6. ChangeId
    7. Deserialize
    8. MoveChildren
    9. UpdateReferences
    10. RemoveOriginalItem


    1. CheckPermissions
    Let's check permissions first. I borrowed this processor from uiRename pipeline and slightly modified that.
    public virtual void CheckPermissions(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
        if (!SheerResponse.CheckModified())
        {
            return;
        }
    
        Item obj = GetItem(args);
        if (obj == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            if (!obj.Access.CanRename() || obj.Appearance.ReadOnly)
            {
                Context.ClientPage.ClientResponse.Alert(Translate.Text("You do not have permission to change ID \"{0}\".", obj.DisplayName));
                args.AbortPipeline();
            }
    
            if (Context.IsAdministrator || !obj.Locking.IsLocked() || obj.Locking.HasLock())
            {
                return;
            }
    
            Context.ClientPage.ClientResponse.Alert(Translate.Text("You cannot edit this item because '{0}' has locked it.", obj.Locking.GetOwnerWithoutDomain()));
            args.AbortPipeline();
        }
    }

    2. VerifyUnsupportedTypes
    At this stage I want to disallow changing IDs for template fields. By that moment I haven't resolved how to change fields accurately, so will avoid that for now,
    public virtual void VerifyUnsupportedTypes(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
    
        Item item = GetItem(args);
        if (item == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            if (item.TemplateID == new ID("{455A3E98-A627-4B40-8035-E683A0331AC7}") || item.Template.Name == "Template field")
            {
                Context.ClientPage.ClientResponse.Alert("You cannot modify ID of template field, but you may do that for template itself");
                args.AbortPipeline();
            }
        }
    }

    3. GetNewName
    Asking user to provide new ID in the popup input box and validate that input value.
    public virtual void GetNewName(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
    
        string header = Translate.Text("Change ID");
        if (args.IsPostBack)
        {
            if (string.IsNullOrEmpty(args.Result) || args.Result == "null" || args.Result == "undefined")
                args.AbortPipeline();
    
            else if (args.Result.Trim().Length == 0)
            {
                Context.ClientPage.ClientResponse.Alert("The name cannot be blank.");
                Context.ClientPage.ClientResponse.Input("Enter new ID for the item:", string.Empty,
                    ID_FORMAT, "'$Input' is not a valid ID.", ID_LENGTH, header);
    
                args.WaitForPostBack();
            }
            else
            {
                Item obj = GetItem(args);
    
                if (obj == null)
                {
                    HandleItemNotFound(args);
                }
                else
                {
                    Context.ClientPage.ServerProperties["NewID"] = args.Result;
                    args.IsPostBack = false;
                }
            }
        }
        else
        {
            Item obj = GetItem(args);
            if (obj == null)
            {
                HandleItemNotFound(args);
            }
            else
            {
                Context.ClientPage.ClientResponse.Input("Enter new ID for the item:", obj.ID.ToString(), ID_FORMAT, 
                    "'$Input' is not a valid ID.", ID_LENGTH, header);
    
                args.WaitForPostBack();
            }
        }
    }

    4. CheckNumberOfReferences
    This processor is not mandatory and presents for the sake of user experience. 
    public virtual void CheckNumberOfReferences(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull((object)args, "args");
        if (args.IsPostBack)
        {
            if (args.Result == "yes")
            {
                return;
            }
    
            args.AbortPipeline();
        }
        else
        {
            Item obj = GetItem(args);
            if (obj == null)
            {
                HandleItemNotFound(args);
            }
            else
            {
                string checkLinksMessage = GetCheckLinksMessage(obj);
                if (string.IsNullOrEmpty(checkLinksMessage))
                {
                    return;
                }
    
                SheerResponse.Confirm(checkLinksMessage);
                args.WaitForPostBack();
            }
        }
    }
    GetCheckLinksMessage is calculating number of references for the item we are changing ID for and prompts that operation may take a longer time once more than 100 references are found,
    private static string GetCheckLinksMessage(Item item)
    {
        Assert.ArgumentNotNull(item, "item");
        string str = string.Empty;
        if (GetLinks(item) > 100)
        {
            str = Translate.Text("This operation may take a long time to complete.\n\nAre you sure you want to continue?");
        }
    
        return str;
    }
    

    5. Serialize
    That is where our main logic begins. Let's serialize original item first using Sitecore API so by default it will write under /Data/serialization folder outside web root.
    public virtual void Serialize(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
    
        Item item = GetItem(args);
    
        if (item == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            try
            {
                Serializer.SerializeItem(item);
            }
            catch (Exception e)
            {
                Context.ClientPage.ClientResponse.Alert($"Failed to serialize item: {item.DisplayName}");
                args.AbortPipeline();
            }
        }
    }

    And Serializer class is quite simple static class:

    public static class Serializer
    {
        public static void SerializeItem(Item item)
        {
            if (item != null)
            {
                Manager.DumpItem(item);
            }
        }
    
        public static void RestoreItem(string serializedItemPath, Database database)
        {
            var options = new LoadOptions(database) {ForceUpdate = true};
    
            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                Manager.LoadItem(serializedItemPath, options);
            }
        }
    }
    

    6. ChangeId
    Then need to locate serialized file and modify it. In order to open I am again relying on Sitecore API by calling PathUtils.GetFilePath(new ItemReference(item)) method so it will return me the same path item was previously serialized. Then I read the file and simply replace old ID to the new ID and save it back. 
    public virtual void ChangeId(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
    
        string newId = StringUtil.GetString(Context.ClientPage.ServerProperties["NewID"]);
        if (string.IsNullOrEmpty(newId) || newId == "null" || newId == "undefined")
        {
            return;
        }
    
        var item = GetItem(args);
    
        if (item == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            try
            {
                string serializedItemPath = GetSerializationPath(item);
    
                if (File.Exists(serializedItemPath))
                {
                    string text = File.ReadAllText(serializedItemPath);
    
                    text = text.Replace(item.ID.ToString(), newId);
    
                    File.WriteAllText(serializedItemPath, text);
                }
            }
            catch (Exception e)
            {
                Context.ClientPage.ClientResponse.Alert("Cannot find serialized file");
                args.AbortPipeline();
            }
        }
    }

    7. Deserialize
    Deserialize file back with modified ID. Sitecore allows multiple items with the same name at the same level so it will end up having two exactly the same items next to it other with new and old IDs.
    public virtual void Deserialize(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
    
        Item item = GetItem(args);
    
        if (item == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            try
            {
                string serializedItemPath = GetSerializationPath(item);
                Serializer.RestoreItem(serializedItemPath, item.Database);
            }
            catch (Exception e)
            {
                Context.ClientPage.ClientResponse.Alert($"Failed to deserialize item: {item.DisplayName}");
                args.AbortPipeline();
            }
        }
    }

    8. MoveChildren
    Since items can have children, the next step obviuosly would be to relocate all children recursively from underneath old item to become the children of a new item. Than especially makes sense when you're modifying a template item.
    public virtual void MoveChildren(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
        var item = GetItem(args);
        if (item == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            try
            {
                //if (item.Paths.FullPath.StartsWith("/sitecore/templates"))
                {
                    string newId = StringUtil.GetString(Context.ClientPage.ServerProperties["NewID"]);
                    var newIdItem = item.Database.GetItem(new ID(newId));
    
                    var children = item.Children;
                    foreach (Item child in children)
                    {
                        child.MoveTo(newIdItem);
                    }                            
                }
            }
            catch (Exception e) 
            {
            }
        }
    }

    9. UpdateReferences
    Apart from having child items, an item can be referenced by other items, the entire data relationship in Sitecore is built on that principle. The good news is that in recent versions of Sitecore references are GUIDs and no more paths. So what is now important - to go through all the referrers and update them with a GUID of new item. Remember step 4 where we made CheckNumberOfReferences processor that counted refs and prompted if there are too many? Those are the links to be modified at this stage:
    public virtual void UpdateReferences(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
    
        var item = GetItem(args);
        if (item == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            try
            {
                string newId = StringUtil.GetString(Context.ClientPage.ServerProperties["NewID"]);
                Relink(item, newId);
            }
            catch (Exception e)
            {
                Context.ClientPage.ClientResponse.Alert("Error while updating links: " + e.Message);
                args.AbortPipeline();
            }
        }
    }

    10. RemoveOriginalItem
    And finally, when everything else is done and we've got new item with all children and references modified. So it is good now o delete an original item
    public virtual void RemoveOriginalItem(ClientPipelineArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
    
        Item item = GetItem(args);
    
        if (item == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            try
            {
                using (new SecurityDisabler())
                {
                    item.Delete();
                }
            }
            catch (Exception e)
            {
                Context.ClientPage.ClientResponse.Alert("Error deliting initial item");
                args.AbortPipeline();
            }
        }
    }  

    There will be also final part of this blog post later that will add unit testing, sum up the results and highlight what can be done to make it better.

    I will ask readers to forgive me at this stage for not giving a package with ChangeId module - haven't it properly tested yet. However anyone curious can clone it from GitHub repo and give a try yourself.