Editing Sitecore item from code: revision and publishing considerations

I’ve noticed some people are struggling with updating items from code and making it publish properly, with Sitecore Publish Service (SPS) in particular. So I thought I’d just share how I usually deal with some of the related issues you may run into. I’m mainly using SPS, so this post is mostly targeting that scenario, but it is probably valid for solutions using the old way of publishing as well. Nothing here is really SPS unique.

Editing items

When modifying items from code, you save the item by calling the item.Editing.EndEdit method. This method comes in three variants:

  • EndEdit(bool updateStatistics, bool silent)
  • EndEdit(bool silent) // same as updateStatistics = true
  • EndEdit() // same as updateStatistics = true, silent = false

But in practice, there are really only two valid combinations. You either update statistics or you don’t. Here’s why:

Updating statistics means that the item will get a new date in the Updated field and will have the current user name written into the UpdatedBy field. The item will also get a new random guid in the Revision field. If update statistics is false, those three fields are left untouched.

Silent means that no events will be fired when saving the item, and that is no events what so ever. Not just the item:saving/item:saved processor you might have added. No cache clearing, no item indexing and no history tracking will be performed either. So save yourself a lot of headaches by never ever set the silent parameter to true, unless you really really know what you’re doing.

So you either call EndEdit() (or EndEdit(true, false) that does exactly the same) or you call EndEdit(false, false). No other combination is viable in practice.

Publishing with no statistics

So given the above, you basically chose to update statistics depending on what kind of operation you’re performing from your code. You may for example do changes to an item based on a command that has been fired by the author and it would make perfect sense to track those changes. You may for example have data updated based on an integration or you do data migration related to a code deploy or something. In such scenario you may want to preserve the existing statistics values instead.

Now, here’s a little gap in my opinion. Even if you’re not updating statistics, you’ve still changed the content of item. That typically needs a new revision. The revision field is a key component when identifying modified items between the master and the web database. SPS relies heavily on this.

A way to solve this is to simply write a new guid in the revision field prior calling EndEdit(false, false), like this:

item[Sitecore.FieldIDs.Revision] = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);

Note that the guid format is different from what we’re used to in Sitecore. In this field it’s lowercase with dashes and without braces. Not the uppercase ID format we normally use. Sitecore Publish Service is also a bit picky about the format of this field, so Sitecore provided us with a separate tool for checking this. In the SPS folder, you can get a report of invalid revision fields by executing the following command:

.\Sitecore.Framework.Publishing.Host.exe diagnostics revision

and the tool can also correct all invalid revision fields by adding fix at the end of the command line. The --help option will give you the documentation of this.

More things can go wrong

So you might have done everything correct by the book, but you may find items still not being published by SPS. There are a few potential causes of this.

Every time an item is saved, an item changed notification is sent to SPS. From what I’ve understood, SPS uses a circuit breaker from the Polly library, that basically discards events if there are a lot of events fired in a short period of time. That can easily happen if you perform batch updates of several items. If SPS misses such events, it wont publish the item changes.

Another cause could be that you’re modifying shared or unversioned fields. If you modify a shared field, you’re in a way modifying all versions on all languages. But since the revision field is versioned, the publishing operation may end up comparing a different language or version and therefore not seeing the changed revision. You may need to write new revision values to several languages and/or versions depending on what changes you do. Obviously a shared field is still a shared field, so it really doesn’t matter what version of such item is published. The key thing here is to make sure SPS picks up the change.

Touching items

Inspired by the unix touch command, I did a little toolset that will just update the revision field of an item. This would also fire all the Sitecore events so that SPS will notice the item as changed and pick it up for publishing if it has previously missed such events.

The code for it is as simple as this:

public static Item TouchItem(Item item)
{
  item.Editing.BeginEdit();
  item[Sitecore.FieldIDs.Revision] = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);
  item.Editing.EndEdit(false, false);
  return item;
}

I’m a big fan of Sitecore Powershell Extensions (SPE), so I also made a simple Cmdlet of it like this:

[UsedImplicitly]
[OutputType(typeof(Item)), Cmdlet("Touch", "Item")]
public class TouchItemCmdlet : Cmdlet
{
  [UsedImplicitly]
  [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
  public Item Item { get; set; }

  protected override void ProcessRecord()
  {
    var item = TouchItemCommand.TouchItem(Item);
    WriteObject(item);
  }
}

With the above, it plays nice with other SPE script, such as:

Get-ChildItem 'master:/sitecore/content/path' -Language * | Touch-Item

And for power authors, I also added it as a simple command in the Content Editor ribbon like this:

Full code of the command

[Serializable]
[UsedImplicitly]
public sealed class TouchItemCommand : Command
{
  public override CommandState QueryState(CommandContext context)
  {
    if (context.Items == null || context.Items.Length != 1)
      return CommandState.Hidden;

    var item = context.Items[0];
    if (item == null)
      return CommandState.Hidden;
    
    if (item.Versions.Count == 0)
      return CommandState.Disabled;

    if (Sitecore.Context.IsAdministrator)
      return CommandState.Enabled;

    if (item.Access.CanWrite() && item.Access.CanWriteLanguage())
      return CommandState.Enabled;

    return CommandState.Disabled;
  }

  public override void Execute(CommandContext context)
  {
    Assert.ArgumentNotNull(context, nameof(context));
    var item = context.Items[0];

    var parameters = new NameValueCollection();
    parameters["id"] = item.ID.ToString();
    parameters["db"] = item.Database.Name;
    parameters["lang"] = item.Language.Name;
    parameters["version"] = item.Version.Number.ToString(CultureInfo.InvariantCulture);

    Sitecore.Context.ClientPage.Start(this, nameof(Run), parameters);
  }

  private void Run(ClientPipelineArgs args)
  {
    Assert.ArgumentNotNull(args, nameof(args));
    var itemId = new ID(args.Parameters["id"]);
    var db = Factory.GetDatabase(args.Parameters["db"]);
    var language = LanguageManager.GetLanguage(args.Parameters["lang"]);
    var version = Sitecore.Data.Version.Parse(args.Parameters["version"]);

    var item = db.GetItem(itemId, language, version);
    if (item == null || item.Versions.Count == 0)
    {
      SheerResponse.Alert("Item not found");
      return;
    }

    TouchItem(item);
  }
  
  public static Item TouchItem(Item item)
  {
    item.Editing.BeginEdit();
    item[Sitecore.FieldIDs.Revision] = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);
    item.Editing.EndEdit(false, false);
    return item;
  }
}

Taking it a step further

If you work a lot with item changes from code, it could be worth writing some ItemEditing extension methods with a more natural footprint, such as an enum instead of two booleans and have a variant where revision is updated but not the other statistics fields.

If you’re working with ReSharper External Annotations, you could also mark the built-in EndEdit methods obsolete, enforcing you and your team to use your extension methods instead. Such approach could help avoid accidentally making “silent” updates or updating items without updating the revision field.